问题来源
1.线程池参数的含义?
int corePoolSize:常驻线程数
int maximumPoolSize:线程池同时执行的最大线程数,>=1
long keepAliveTime:空闲线程的存活时间
TimeUnit unit:keepAliveTime的单位
BlockingQueueworkQueue:被提交等待被执行的任务
ThreadFactory threadFactory:工作线程的线程工厂
RejectedExecutionHandler handler:线程池拒绝策略
线程池拒绝策略
1 | 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时, |
2.innodb的索引实现
1.表数据文件本身是按照B+tree组织的一个索引文件结构
2.聚集索引叶子结点包含了完整的数据记,这个索引的key是数据表的主键
3.为什么是B+tree?
其实选择B+tree是因为树的高度小,Hash有个致命的缺点就是浪费内存,如果采用二叉树,那么当数据量越大的时候,这棵树就越深,树越深IO的次数就会越多(提高系统效率的两种方式:1、减少IO次数 2、减少IO量)
根据上述分析得出我们需要得出结论:我们需要找一个有多个分支且有序的多叉有序树
4.操作系统虚拟内存换页的过程是什么?
1.如果内存中有空闲的物理页面,则分配一物理页帧r,然后转第4步,否则转第2步;
2.选择某种页面置换算法,选择一个将被替换的物理页帧r,它所对应的逻辑页为q,如果该页在内存期间被修改过,则需把它写回到外存;
3.将q所对应的页表项进行修改,把驻留位置0;
4.将需要访问的页p装入到物理页面r中;
5.修改p所对应的页表项的内容,把驻留位置1,把物理页帧号置为x;
6.重新运行被中断的指令。
5.线程池大小与 CPU 处理器的利用率之比可以用下面公式估算
CPU密集型多为cpu运算频繁的:设置CPU核数+1
IO密集型:设置cpu核数*10
1 |
|
6.Redis的使用—分布式锁的实现
1.数据库乐观锁;
2.基于Redis的分布式锁;
3.基于ZooKeeper的分布式锁
4.redisson的红锁
一 基于数据库
a.数据库建一张表,字段方法名并且作为唯一性,当一个方法执行时插入,则相当于获得锁,其他线程将无法访问,方法执行完则释放锁。
但是上面这种存在问题:
1、数据库单点,出现故障则将导致系统不可用。
2、没有失效时间,一旦操作方法异常,导致一直没有解锁,也将导致其他不可用用。
b.使用select * from user u where username = ‘’ for update 来对记录加上排他锁。操作完成后使用commit命令释放锁。
二基于缓存 redis
1 | public class RedisTool { |
三基于zk
大致思路:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上,所以性能上不如基于缓存实现。
综合比较:1.3性能低,推荐redis
如果对数据有强一致性要求,不能放缓存
7.TCP 三次握手和四次挥手
三次握手只是在建立连接。三次握手之后,才有资源的开辟。可以开始传输数据了。
C -> S (syn, seq=j) C 说,我想连接
S -> C (syn+ack, ack=j+1, syn=k) 发完之后,C 知道了 S 能收到自己的消息
C -> S (ack, ack=k+1) 发完之后,S 知道了 C 能收到自己的消息(确认是双向的),这就是为什么需要第三次握手
三次握手之后,双方开辟资源,建立了 socket,实际应用时,第三次握手包和发送的数据包是粘连在一起的。
如果类比三次握手,在第二次挥手的时候同时发 FIN + ACK 明显不合理,因为被动方可能没有数据发送完,你这么关太草率了,所以需要四次。
①客户端发送报文===>
②服务端收到报文,结束监听,返回一段报文
③客户端确认收到TCP报文,并返回最后一段TCP报文
即SYN建立连接报文与ACK确认接收报文是在同一次”握手”当中传输的,所以”三次握手”不多也不少,正好让双方明确彼此信息互通
所谓的四次挥手即TCP连接的释放(解除)。连接的释放必须是一方主动释放,另一方被动释放
都是由客户端发起
8.为什么四次分手之后,还会等两个传输时间,才会释放资源
因为如果最后 C 端返回的 ACK 号丢失了,这时 S 端没有收到 ACK,会重发一遍 FIN,如果此时客户端的套接字已经被删除了,会发生什么呢?套接字被删除,端口被释放,这时别的应用可能创建新的套接字,恰好分配了同一个端口号,而服务器重发的 FIN 正好到达,这个 FIN 就会错误的跑到新的套接字里面,新的套接字就开始执行断开操作了。为了避免这样的误操作,C 端会等几分钟再删除套接字。
9.volatile关键字的作用
被volatile修饰的变量在编译成字节码文件时会多个lock指令,该指令在执行过程中会生成相应的内存屏障,以此来解决可见性跟重排序的问题。
1.解决的是多核CPU带来的缓存与CPU之间数据的可见性
JMM:java内存模型
1.线程解锁前,必须把共享变量刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁与解锁必须是同一把锁
volatile实现内存指令重排,保证可见性和禁止指令重排,
可保证一段内存中一个变量的原子性,原生类型都是原子性的。所以java中 volatile long,volatile double都是线程安全的
10.乐观锁,悲观
乐观锁(Optimistic Lock):
1 | 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 |
悲观锁(Pessimistic Lock):
1 | 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。 |
11.HashMap结构,是否线程安全?ConcurrentHashMap如何保证线程安全
HashMap在java1.7之前底层数据结构是数组+链表,1.8之后是数组+链表+红黑树,
在1.7以前的put方法采用的是头插法,当hash碰撞次数到达8,且桶内元素到达64个的时候形成链表,但是在极端情况下会造成链表过长,效率变低,并且在rehash的时候,头插法会造成回环链首尾相连,形成死锁,在java1.8以后采用红黑树,除了添加效率都高,是线程不安全的,不安全示例
1 | public class HashMapTest { |
1.通常代替HashMap的安全由HashTable代替,但是多线程下他的put.get方法都是synchronized,效率太低,
2.Collections.synchronizedMap(),底层仍是synchronized
3.ConcurrentHashMap 与 ConcurrentSkipListMap
ConcurrentHashMap 加锁
ConcurrentSkipListMap 不需要加锁,浪费空间,
4.ConcurrentHashMap
ConcurrentHashMap如何保证线程安全,在1.7以前由划分segment分段锁机制,共计16个并发级别,隔离级别太大,有很多空间就浪费了,太小就段内的元素过多
1.8以后是cas算法C语言写得,无锁算法,put添加的时候,链表+红黑树
put方法(无锁添加)
12.之前用过哪些设计模式
目前项目再用的是责任链设计模型,像动态代理,装饰者,工厂模式,在Spring的源码中都有体现,责任链模式旨在降低处理请求流程的耦合
责任链模式
13.滑动窗口
题目描述
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和≥ s的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
1 | //解析 滑动窗口 Sliding Window |
14.Btree和 B+tree的优缺点
B+树中的B代表平衡(balance),而不是二叉(binary)
二叉查找树
二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。
平衡二叉树
如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
B+Tree
B+Tree相对于B-Tree有几点不同:
非叶子节点只存储键值信息。
所有叶子节点之间都有一个链指针。
数据记录都存放在叶子节点中。
查询速度快,但是占用空间
索引结构:B-Tree B+Tree B:balance
B-Tree:平衡二叉树
特点:
1.具有数据节点
2.指向下层指针
3.指向数据指针
缺页查询,产生IO
B+Tree:
特点:
1.具有数据节点
2.指向下层指针
命中数据3层查找后查询数据指针
加载更快,产生更少IO
效率:BTree更高,但从IO角度,Mysql选择B+Tree
15.说一下HashMap的实现,扩容机制,扩容时如何保证操作
put 方法比较复杂,实现步骤大致如下:
1、先通过 hash 值计算出 key 映射到哪个桶。
2、如果桶上没有碰撞冲突,则直接插入。
3、如果出现碰撞冲突了,则需要处理冲突:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。
(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红
黑树。
4、如果桶中存在重复的键,则为该键替换新值。
5、如果 size 大于阈值(8),则进行扩容
根据hash算法得到hash码值,也就是数组的索引值,在判断是否有对象,如果没有则放入
如果有则先通过equals比较两个对象的内容,如果内容一样,则覆盖value,
如果内容不一样,形成链表,1.7后加的放前面,这种情况叫做hash碰撞,这种情况我们是尽可能避免的,如果这里的元素过多的话,插入效率过低,为了避免的话,重写hashcode和equals方法保持一致,这种情况避免不了
加载因子,当到达元素个数的0.75,进行扩容,扩容则每个元素重新运算位置,,如果到达100%其他位置可能会不存入,如果太小,则频繁扩容,可浪费空间。这样碰撞的概率会降低,但是极端情况下还是需要查询每个元素比较,效率极低。
1.8以后,数组+链表+红黑树
当碰撞的个数大于8时,并且总容量大于64时,将链表转为红黑树,除了添加以外其他的效率都高,jdk1.8加到链表末尾,扩容以后不需要运行hash算法计算hashcode值。原来hash表的总长度,加上hash表的现在的位置,就放到第8个位置即可。
3.redis扩容机制(渐进式单线程扩容)
Redis是一个键值对(key-value pair)数据库服务器,Redis服务器结构是redis.h/redisServer结构表示,Redis服务器中的所有数据库保存在db数组中,数据库的结构是redis.h/redisDb,其中,redisDb结构的dict字典保存了数据库中的所有键值对,所以,说起Redis的扩容机制,指的就是字典中哈希表的rehash(重新散列)操作
在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。
渐进式rehash 的详细步骤:
1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束
采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。
16.SpringAop,ioc的原理,如何解决循环依赖
SpringIoc可以对我们应用程序中的java对象做一个集中化的管理,从而使我们从繁琐的new Object();中解脱出来
Spring中AOP的有两种实现方式:
1、JDK动态代理
2、Cglib动态代理
在没有修改原有类的代码的情况下,对原有类的功能进行了增强
静态代理模式:静态代理说白了就是在程序运行前就已经存在代理类的字节码文件,代理类和原始类的关系在运行前就已经确定
动态代理模式:动态代理类的源码是在程序运行期间通过JVM反射等机制动态生成,代理类和委托类的关系是运行时才确定的
使用jdk生成的动态代理的前提是目标类必须有实现的接口。但这里又引入一个问题,如果某个类没有实现接口,就不能使用JDK动态代理,所以Cglib代理就是解决这个问题的
Cglib使用的前提是目标类不能为final修饰。因为final修饰的类不能被继承。
核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。
通过定义和前面代码我们可以发现3点:
1.AOP是基于动态代理模式。
2.AOP是方法级别的(要测试的方法不能为static修饰,因为接口中不能存在静态方法,编译就会报错)。
3.AOP可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。
循环依赖解决
1.在字段上使用@Autowired注解,让Spring决定在合适的时机注入
2.用基于setter方法的依赖注入
17.两个线程对变量i进行加1操作,结果如何?为什么?如何解决?
1 | public static int i=0; |
线程安全问题,对共享变量进行修改
改进方法1:
1 | public static volatile int i=0; |
synchronized在多jvm情况下不生效,且效率低下
方法2
1 | private static AtomicInteger num = new AtomicInteger(0); |
方法3
1 | public static volatile int i=0; |
18.CAS概念,原子类实现
CAS锁是比较偏重的操作?
CAS在操作锁时,执行比较并交换操作,相对synchronized瘦锁是比较重的锁,偏向锁在这里避免了CAS操作。UseBiaseLocking对synchronize有用
比较并交换,判断取出内存中某时刻的数据并在当下时刻进行交换,缺点:循环时间长,只能保证一个共享变量的原子操作,引来ABA问题?
CAS核心是由native修饰的Unsafe类,其中valueOff为内存偏移量地址,变量由volatile修饰。
private static final Unsafe unsafe
private volatile int value;
unsafe类是CAS的核心类,是由C语言native方法来访问的
1 | public final int getAndAddInt(Object var1, long var2, int var4) { |
其中getAndAddInt方法,this表示当前对象valueoffset表示内存中的偏移地址,delta是当前value增加的变量
CAS即比较当前值与预设值,交换并增加,如果与预想一致就交换,否则再次自旋,所以带来循环开销问题,进而引来ABA问题。
原子类的话经典类:AtomicInteger,其共享变量是由volatile修饰的,
getAndIncrement是unsafe类操作,底层也是cas
1 | public final int getAndIncrement() { |
synchronized
19.synchronized和Lock有什么区别?
①:synchronized是JVM层面实现的,java提供的关键字,Lock是API层面的锁。
②:synchronized不需要手动释放锁,底层会自动释放,
Lock则需要手动释放锁,否则有可能导致死锁
③:synchronized等待不可中断,除非抛出异常或者执行完成
Lock可以中断,通过interrupt()可中断
④:synchronized是非公平锁
Lock是默认公平锁,当传入false时是非公平锁
⑤:synchronized不可绑定多个条件
Lock可实现分组唤醒需要唤醒的锁
monitorenter
monitorexit
synchronized通过监控对象来完成,本质是锁一个对象
同步方法
1 | public class Demo { |
修饰方法与修饰代码块产生字节码不同
如何实现lock,AQS:AbstractQueuedSynchronizer,AQS是ReentrantLock的核心实现
1 | final boolean nonfairTryAcquire(int acquires) { |
ReentrantLock的子类Sync类的final static子类FairSync和NonFairSync用于支持公平锁和非公平锁。
AQS的tryAcquire()和FairSync的tryAcquire()判定是否为公平锁,其实现也是偏向锁UseBiaseLock的实现
1 | protected boolean tryAcquire(int arg) { |
该方法首先会判断当前线程的状态,如果c==0 说明没有线程正在竞争锁。(反过来,如果c!=0则说明已经有其他线程已经拥有了锁)。如果c==0,则通过CAS将状态设置为acquires(独占锁的acquires为1),后续每次重入该锁都会+1,每次unlock都会-1,当数据为0时则释放锁资源。其中精妙的部分在于:并发访问时,有可能多个线程同时检测到c为0,此时执行compareAndSetState(0, acquires))设置,可以预见,如果当前线程CAS成功,则其他线程都不会再成功,也就默认当前线程获取了锁,直接作为running线程,很显然这个线程并没有进入等待队列。如果c!=0,首先判断获取锁的线程是不是当前线程,如果是当前线程,则表明为锁重入,继续+1,修改state的状态,此时并没有锁竞争,也非CAS,因此这段代码也非常漂亮的实现了偏向锁。
20.AQS有什么特点
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
1 | protected final int getState() { |
2、AQS设计思想
使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。
利用int类型标识状态。在AQS类中有一个叫做state的成员变量
1 | /** |
基于AQS有一个同步组件,叫做ReentrantLock。在这个组件里,stste表示获取锁的线程数,假如state=0,表示还没有线程获取锁,1表示有线程获取了锁。大于1表示重入锁的数量。
继承:子类通过继承并通过实现它的方法管理其状态(acquire和release方法操纵状态)。
可以同时实现排它锁和共享锁模式(独占、共享),站在一个使用者的角度,AQS的功能主要分为两类:独占和共享。它的所有子类中,要么实现并使用了它的独占功能的api,要么使用了共享锁的功能,而不会同时使用两套api,即便是最有名的子类ReentrantReadWriteLock也是通过两个内部类读锁和写锁分别实现了两套api来实现的。
3、AQS的大致实现思路
AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。