今天不学习,明天变垃圾
参考:线程池执行流程及拒绝策略
答: ① 线程池执行流程:
当任务来了之后,线程池的执行流程是:先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行,否则则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务,否则将执行线程池的拒绝策略
② 拒绝策略:
当任务过多且线程池的任务队列已满时,此时就会执行线程池的拒绝策略,线程池的拒绝策略默认有以下 4 种:
AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
DiscardPolicy:忽略此任务,忽略最新的一个任务;
DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
默认的拒绝策略为 AbortPolicy 中止策略。
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁。
缺点: 如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源。 (而挂起等待的时候是 不消耗 CPU 的)。
1.乐观锁Vs悲观锁
2.普通互斥锁 VS 读写锁
3.轻量级锁VS重量级锁
4.自旋锁VS挂起等待锁
5.公平锁VS非公平锁
6.可重入锁VS不可重入锁
你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
答: ① 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。
② 乐观锁认为多个线程访问同一个共享变量冲突的概率不大, 并不会真的加锁, 而是直接尝试访问数据。 在访问的同时识别当前的数据是否出现访问冲突。
③ 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数, 获取不到锁就等待。
④ 乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。
介绍下读写锁?
答: ① 读写锁就是把读操作和写操作分别进行加锁.
② 读锁和读锁之间不互斥.
③ 写锁和写锁之间互斥.
④ 写锁和读锁之间互斥.
⑤ 读写锁最主要用在 “频繁读, 不频繁写” 的场景中.
什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
答: ① 自旋锁即:如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止。 第一次获取锁失败, 第二次的尝试会在极短的时间内到来; 一旦锁被其他线程释放, 就能第一时间获取到锁。
② 相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用。
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源。
synchronized 是可重入锁么?
答: ① 是可重入锁.
② 可重入锁指的就是连续两次加锁不会导致死锁.
③ 实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数)。如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增。
对于synchronized:
①既是乐观锁,也是悲观锁
②既是轻量级锁,又是重量级锁
③乐观锁的部分是基于自旋锁实现的,悲观锁的部分是基于挂起等待锁实现的
所以:
synchronized是自适应的:初始使用的时候是乐观锁/轻量级锁/自旋锁,如果锁竞争不激烈就保持上述状态不变;但是如果锁竞争激烈了,synchronized就会自动升级成悲观锁/重量级锁/挂起等待锁。
④不是读写锁,而是普通互斥锁
⑤是非公平锁
⑥是可重入锁
(在标准库中是有另外的其他锁能够实现④⑤的)
boolean CAS(address, expectValue, swapValue) {
// address:内存地址
// expectValue:比较寄存器A
// swapValue:交换寄存器B
if (&address == expectedValue) {
&address = swapValue;
//这里的赋值其实就是“交换”。
//但是其实并不关心寄存器B里的是啥,更关心的是内存中是啥!
//所以把交换近似看成是赋值其实也没毛病。
return true;
}
return false;
}
// 以上的这一组操作是通过硬件实现的,一个CPU指令完成的,也就是说是原子性的!
// 则CAS是线程安全的!! 同时还高效。(高效:因为不涉及到锁冲突+线程等待)
// 故:基于CAS实现一些逻辑的时候即使不加锁也是可以实现线程安全的!
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
① 如前面讲过的count++,在多线程环境下线程是不安全的,要想线程安全,就需要加锁,但是加锁后性能就会大打折扣;所以,我们就可以基于CAS操作来实现“原子”的++,从而保证线程的安全和高效。
② 标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的。
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作。
③ 伪代码的实现:
class AtomicInteger {
// AtomicInteger : 这个其实是在标准库中已经封装好的一个类
private int value;
public int getAndIncrement() {
// getAndIncrement() :这个方法就相当于后置++
int oldValue = value;
// 此处的oldValue相当于是寄存器A,是把内存中的value值读取到寄存器中!
while ( CAS(value, oldValue, oldValue+1) != true) {
// 把(oldValue+1)理解成时另外一个寄存器B的值
// 比较看内存中的value值是否和寄存器A的值相同,如
//果相同就把寄存器B的值给设置到value内存中,同时
//CAS返回true,结束循环;
//如果不相同,就无事发生,CAS返回false,进入循环
//体里,重新读取内value值到寄存器A中。
// 其实就类似于给内存做个标记,使用寄存器A来检查该内存值是不是之前的值,也就是有没有被修改。
//(寄存器A就是标记)!!
oldValue = value;
}
return oldValue;
}
}
④ 注:CAS 是直接读写内存的, 而不是操作寄存器。
public class SpinLock {
private Thread owner = null;
// owner : 当前这把锁是哪个线程获取到的,null就是锁是无人获取的状态/解锁状态。
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
// 比较owner和null是否相同(是否为解锁状态),如果是就进行交换,把当前调用lock的线程的值设置到owner里,相当于此时加锁成功,同时结束循环。
// 如果owner不为null,则CAS不进行交换,返回fasle,会进入循环,此时会立即再次发起判定。
// 也就是如果锁没有线程占用就占用,如果被占用就反复询问,一解锁就及时加锁。
}
}
public void unlock (){
this.owner = null;
}
}
【synchronized原理】主要讨论的是synchronized背后做的事情
2)轻量级锁
① 此处的轻量级锁就是通过 CAS 来实现。
通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
如果更新成功, 则认为加锁成功
如果更新失败, 则认为锁被占用,继续自旋式的等待(并不放弃 CPU).
② 自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
③ 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了。也就是所谓的 “自适应”。
3)重量级锁
① 如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
② 此处的重量级锁就是指用到内核提供的 mutex .
③ 执行过程:
- 执行加锁操作, 先进入内核态;
- 在内核态判定当前锁是否已经被占用; 如果该锁没有占用, 则加锁成功, 并切换回用户态;
- 如果该锁被占用,则加锁失败,此时线程进入锁的等待队列, 挂起。 等待被操作系统唤醒。
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁。
如果当前场景中锁竞争不激烈,则是以轻量级锁状态来进行工作(轻量级锁是通过自旋来实现的,可以第一时间拿到锁);
如果当前场景中锁竞争激烈,则是以重量级锁状态来进行工作的(重量级锁通过挂起等待来实现,可能拿到锁每那么及时,但是节省了CPU的开销)
synchronized还有其他的优化手段:(一是锁升级/锁膨胀,二是锁消除,三锁粗化)
什么是偏向锁?
答: 偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程)。 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销。 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态。
synchronized 实现原理 是什么?
答: 参考【synchronized原理】所有内容:特点+加锁过程+优化手段。