1.线程池模型
netty实战中讲到的线程池模型可以描述为:1.从线程池中选择一个空间的线程去执行任务,2.任务完成时,把线程归还给线程池。这个模型与连接池类似。
根据jdk源码的研究,具体的实现模型是,线程池ThreadPoolExecutor中有一个静态内部类Worker,使用装饰器模式扩展了普通任务线程的功能,除了执行封装的任务,还会主动拉取工作队列中的任务来执行,因为线程本身最清楚自己什么时候空闲。而基本的模型则是由线程池去判断线程是否空闲,进而提交任务给空闲线程
2.lock指令
lock指令使CPU的工作内存写入公共内存,并使其他的CPU和内核本身的工作内存中变量的副本无效,并加载公共内存中变量值到CPU缓存来使用
如果没有实际的线程竞争,但是synchronized照样执行加锁解锁,解锁时会把CPU的工作内存同步到公共内存,有开销。
3.线程为什么只能在同步块中调用wait方法?
wait()的原义是临时释放锁,notify(),notifyAll()的原义是唤醒因为加锁失败而阻塞的线程,所以必须在同步块在执行,同时同步块也能保证共享变量对之后获取锁的线程立刻可见。
线程在调用了wait方法等待,被唤醒后依然需要等待获取锁才能继续往下执行。
4.为什么用notifyAll()而不用notify()
在线程调用notifyAll()唤醒所有线程时,如果线程数量太多,但是任务很少,这个线程从waiting状态放到可运行队列,然后一个一个转换成运行状态为一次上下文切换,因为大量线程不需要执行任务,所以转换为运行状态后又立刻转换为等待状态,就会大量的切换上下文。
5.偏向锁和轻量级锁
(1)偏向锁
偏向锁有三种状态:匿名偏向,可重偏向和已偏向,撤销偏向锁后变成不可偏向。在深入理解Java虚拟机的13.3.5节偏向锁的图13-5中,是通过对象头的epoch,将带有无效 epoch 的有偏向对象转换为可偏向但未偏向的对象,同时可重偏向意味着对象没有被锁定,因此epoch可用于判断对象是否锁定。
当对象不能重偏向且另外一个线程时会撤销偏向锁,JVM内部有批量重偏向功能。
(2)轻量级锁
加锁:
假设T1线程在获取了对象锁之后,想再次执行CAS获取锁,发现对象头已经指向当前线程的锁记录,则继续执行,如果失败,说明有T2线程,会把对象头变为重量锁指针,对象头信息还保存在T1的锁记录中,hasCode应该能对应找到锁的对象。加锁失败是对于T2而言。
解锁:
假设T1线程执行CAS,执行失败,说明有T2线程把对象头膨胀为重量级锁指针。解锁失败是对于T1而言,重量级锁需要从用户态变成内核态来使用互斥量,性能低
6.加锁和解锁的指令
使用monitorenter指令获取对象锁时,实际上是通过内核态访问互斥量来判断,如果对象已被加锁则计数器加1并阻塞,执行monitorexit指令或解锁,这时应该会转化为内核态并提示CPU可以释放锁了
7.重排序的类型
重排序有3种:1.编译器重排序,2.指令级重排序,3.内存系统重排序,其中2和3属于处理器重排序(由于某种原因导致重排序),而内存系统重排序是因为本地内存没有及时刷新到主内存的可见性导致重排序。
1.编译器重排序:
这里的重排序是真的重排序,为了优化指令的顺序被重新排列,
2.指令级重排序:
这里先介绍下CPU指令执行,CPU中有很多指令执行单元,例如加法执行单元,减法执行单元等,有些指令可以在CPU并行执行,有些只能在串行执行,这时候为了提高效率,把指令分配到合适的指令执行单元,所以有些并行执行的指令就会比串行指令先执行完,但实际上CPU从内存获取指令的顺序并没有被改变,此顺序是已经被编译器重排序过的了。所以指令级重排序优化是为了让指令更好的并行,就类似线程的并行。
3.内存系统重排序:
例如当2个线程并行共享一个变量时,由于处理器1的写是写入自己的本地内存中,当处理器2读取该共享变量时,由于处理器1的写并没有写入主内存中,所以处理器2读到的是没有被处理器1修改的初始值。处理器对内存的读/写操作的执行顺序, 不一定与内存实际发生的读/写操作顺序一致。
8.CPU和主内存交互的开销
由于CPU与主内存交互是需要独占数据总线的,独占时其他CPU不能访问,所以与主内存交互是开销比较大的操作,为了提高性能,大部分情况下都是直接与本地内存交互,只有在必要的情况下才会把本地内存刷新到主内存。
9.验证工作内存的存在
点击查看代码
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
/*当主线程修改变量后,有些线程会去读到新值而停止,
有些读取不行,就一直运行*/
while(!ready) {
System.out.println(Thread.currentThread().getName()
+":" +System.currentTimeMillis());
Thread.yield();
}
System.out.println(Thread.currentThread().getName() +":" + number);
}
}
public static void main(String[] args) {
for(int i=0;i < 100;i++) {
new ReaderThread().start();
}
number = 42;
//修改共享变量
ready = true;
System.out.println("ready已改");
}
10.StoreLoad屏障优化
因为读线程一般大于写线程,在volatile写后加StoreLoad屏障,而不是在volatile读前加StoreLoad屏障,这样内存屏障的数量与写线程的数量一致,比在读线程加屏障要少,提高执行效率
11.happens-before
happens-before是一个JMM面向程序员的规则,JMM隐藏底层的实现细节,减少程序员理解的难度。
12.CAS操作
Java通信的4种方式都遵循volatile写-读所具有的锁的内存语义,因为CAS操作是读-改-写,所以放在其他操作后面,可以作为volatile读,放在其他操作前,可以作为volatile写
13.final与内存屏障
final因为已经加了StoreStore屏障,确保final域在构造函数结束前已初始化完,所以只要对象引用没有在构造函数逸出,就可以确保构造函数结束之后final域已初始化完。如果没有内存屏障,就算引用没有逸出,也不能确保已初始化。
14.compareAndSetTail()
compareAndSetTail(),只是在unsafe类定义好的存放tail指针的内存地址做CAS操作,所以在传入Node节点作为参数时,只是用到了对象的地址,而没有用到对象本身
15.addWaiter()和enq()自旋
在addWaiter()和enq()自旋时使用了node.prev = t,这是一个volatile写,然后执行CAS操作,也许是为了实现锁的内存语义,因为有内存屏障,确保之前的修改立刻刷新到主内存中。且用到了Effective Java中优化volatile变量读取次数的方式
16.非公平锁
非公平锁线程连续获取锁,是因为其他线程都处于阻塞状态,当释放锁时虽然能唤醒等待线程,但当前线程已经在执行,获取同步状态的速度比别的线程快。
17.synchronized和lock的通知机制的区别
在使用Synchronized实现等待/通知机制时,如果生产者有2个或者消费者有2个,为了防止生产者只唤醒生产者,或者消费者只唤醒消费者而引起死锁,需要使用notifyAll()。在jdk新的锁框架中,使用lock中2个condition来实现,一个condition负责生产者队列,一个负责消费者队列,在唤醒时,确保能正确唤醒生产者/消费者而不至于死锁。
18.volatile语义
volatile增加的语义是阻止volatile变量与普通变量重排序,同时确保在volatile变量被读之前所有的缓存已回写到工作内存中。
19.锁顺序死锁
由于锁顺序导致的死锁问题,恰好说明了Effective Java中避免过度同步,而应该使用开放调用来使更易于找出获取多个锁的代码路径。
开放调用指的是调用的方法不是同步方法,不需要持有方法对应对象的内置锁,但可以在方法内部使用同步代码块来持有锁。这样的方法,能在方法内部调用外部方法时尽快的释放锁,从而减少同时获取多个锁的代码。
20.等待操作完成时尽量不要持有锁
如果在等待操作完成的同时持有该服务的锁,可能由于获取多个锁而产生死锁,所以需要通过一些协议(而不是通过加锁)来让服务处于“持有锁”或“不可用”状态,此时等待操作完成的方法是开放调用,开放调用完成后,通过协议让执行关闭操作的线程才能访问服务的状态。
例如synchronized f(){//等待操作完成}改成f(){//等待操作完成},此时调用该方法不需要持有锁,就不会有死锁的风险,同时通过一些协议来防止其他线程访问关闭中的服务
21.丢失的唤醒信号
丢失的唤醒信号,有两种情况可能导致信号丢失
1.在没有测试条件谓词就直接调用wait()的情况,另一个线程已经把条件谓词更改为true,调用notify()并阻塞,因为条件谓词已经为true,不会再有线程调用notify(),所以调用wait()的线程会永远阻塞。
2.先检查后执行,条件谓词并没有在获取锁之后再检查,而是获取锁之前就检查了,此时同样是在检查为false后,另一个线程更改为true,调用notify()并阻塞,此时该线程会由于已经检查过条件谓词为false(实际已经被更改为true)而调用wait()并永远阻塞。
Java并发编程实战中只讲了第一种场景,是因为他是在已经用锁把条件谓词涉及的状态都保护起来的前提下,不可能存在检查为false之后又被其他线程更改为true的情况。
22.自定义阀门类
在14.2.5节的自定义阀门类中,如果单纯判断open状态,当调用notifyAll()的线程快速关闭后,被唤醒的线程可能因为当前状态是已关闭而再次阻塞,可能此时所有的线程都被阻塞。所以增加了一个计数器,来证明在线程阻塞过程中,有其他的线程调用过open,那么即使因为快速关闭状态不为真,但计数不一致说明可能有其他线程打开后又快速关闭,从来确保线程不会永远阻塞。
23.线程通信的别名
线程通信是比较广泛的叫法,但是如果换一种名字去思考,可能会比较容易了解原理,状态依赖性管理(条件谓词)和条件队列
24.单例模式的延迟初始化占位类模式
单例模式的延迟初始化占位类模式,他的作用类似final变量,因为在类由JVM控制只加载一次,之后不能再被改变,这一次加载由JVM内部用锁来控制并发访问
25.volatile变量可见性的限制
在对volatile变量的读写中,内存屏障只能保证在线程A执行完volatile变量前的操作时,对线程Bvolatile变量后的操作的立即可见性,但如果先执行线程B的volatile变量后的操作,再执行线程A的volatile变量前的操作,就未必可见,因为屏障前和屏障后的操作在两个CPU上执行,而CPU是并行执行的,并不能保证执行顺序。
26.非阻塞队列的性能优化
非阻塞队列中,HOPS的值针对的是单个线程,假如有一个线程A,HOPS值为5,由于有其他3个线程更新尾节点成功,且循环次数都少于5,那么都不会更新尾节点,此时线程A会一直循环直到成功,但循环的次数依然少于5,那最后所有线程都执行完了,但尾节点指针还是没有被更新。
从队列的尾指针位置的角度看,尾指针离后尾节点确实是少于5个位置,正如变量含义,距离超过5再更新尾指针,减少了volatile变量的写操作
27.fork/join框架是对线程的一种抽象
通过fork()和join()这两个方法的简单调用返回实现了把任务拆分成子任务在其他线程执行,封装了对线程的处理,使用户程序看起来只是简单的方法调用。