Java并发编程的艺术摘要
- 线程上下文切换
- 基础概念
- 一. volatile实现原理
- JMM内存屏障
- volatile使用
- happens-before原则
- 二. synchronized关键字
- 实现原理
- 性能损耗
- 锁升级
- 偏向锁
- 偏向锁状态:
- 撤销
- 个人理解
- 轻量级锁
- 轻量级锁解锁
- 个人理解
- 三、CAS
- 四、线状态
- 五、ReentrantLock
- 读写锁
- 锁降级:
- 六、java synchronized锁升降级过程
此文参考《Java并发编程的艺术》和部分《深入理解Java虚拟机》内容,如有问题请随时指正。
线程上下文切换
线程waiting状态也会有时间片切换,只要有线程就会有时间片切换(并发编程艺术1.1.4)。但是在《深入理解JVM虚拟机》书中12.4.3中,介绍线程状态说,waiting状态CPU不会分配执行时间?个人理解是cpu会切换,但是发现是waiting状态的话不会执行,会再次分配给其他线程。
基础概念
- 缓存行:CPU高速缓存中可以分配的最小存储单位。
- 内存屏障:一组处理器命令用于实现内存操作顺序限制
一. volatile实现原理
汇编代码中,volatile修饰的属性操作时有lock前缀,lock指令在多核处理器下有两个作用:
1)将当前处理器缓存行的数据写回到系统内存中;
2)这个写回操作会是在其他cpu中缓存了该内存地址的数据无效:通过缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期。
JMM内存屏障
storeStore loadLoad storeLoad loadStore
防止代码编译、指令执行的重排序
volatile使用
- Doug lea写的LinkedTransferQueue:
头节点和尾节点使用内部类,内部类将共享变量追加到64字节,为了防止cpu缓存行将头节点和尾节点读取到同一个缓存行中,如果在一个缓存行,在操作头尾节点时会相互锁定。
- 单例的双重校验模式,需要使用volatile修饰instance属性,防止指令重排序导致的创建多个实例。
happens-before原则
操作结果可以被后续操作看到,不一定是执行顺序严格保证前后执行。
二. synchronized关键字
- 对于普通的同步方法,锁的是当前对象
- 静态同步方法,锁的当前类的Class对象
- 对于同步代码块,锁的Synchronized括号里配置的对象
实现原理
Monitor对象,任何对象都有Monitor与之关联,对应monitorenter和monitorexit指令,在线程执行monitorenter指令时或尝试获取Monitor对象的所有权。
性能损耗
主要在于阻塞的实现,挂起线程和恢复线程操作需要转入内核态中完成。
锁升级
jdk 1.6优化,增加偏向锁、轻量级锁机制。在对象头mark word(hashcode和锁标识位)中,有锁标识位。
顺序:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁
偏向锁状态:
- mark word中是否存在当前线程id,存在则说明已经获取到了偏向锁
- 不存在当前线程id,则判断偏向锁标志是否为1,不为1,尝试CAS设置为1并将偏向锁指向当前线程;为1的话,尝试竞争偏向锁将其指向当前线程。
- 竞争成功(当前指向的线程id为空,即偏向锁取消了)
- 竞争失败,锁升级为轻量级锁竞争
撤销
全局安全节点时,没有字节码正在执行,偏向锁的线程不处于活动状态,则撤销偏向锁:并不是取消偏向锁的标识位,而是将偏向的线程id置为空。换言之只要线程还处于活动状态就不会撤销偏向锁偏向的线程id。
个人理解
偏向锁没有撤销前有线程竞争就会膨胀为轻量级锁。
轻量级锁
在执行同步代码块前,会在线程的栈帧中创建存储锁记录的空间,并将mark word复制到锁记录中,官方称为Displaced Mark Word,然后线程尝试CAS将对象头中的Mark Word替换为指向锁记录的指针。成功,获取锁;失败,有其他线程竞争锁,尝试自旋方式获取锁。
升级阈值:默认自旋10次会升级到重量级锁,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。JDK1.7之后次数由jvm自行调整,用户不能配置此参数。
轻量级锁解锁
线程执行完成解锁时会将Displaced Mark Word替换回对象头中,如果成功表示没有竞争,如果失败表示当前锁存在竞争,锁会膨胀为重量级锁。
个人理解
只要存在线程同时竞争就膨胀为重量级锁。
对比偏向锁:
偏向锁注重相信不存在竞争,同一个线程会多次重复获取到锁,如果有其他线程需要锁且没有撤销锁的状态就会膨胀,不管是否同时竞争。轻量级锁可以使用CAS+自旋来替换锁拥有的线程,但是如果有同时竞争的线程就会膨胀。
三、CAS
Unsafe 的CAS操作也是使用lock指令,多核cpu加lock,单核不加lock指令。
四、线状态
- New,线程创建尚未启动。
- Runable,包括操作系统线程状态中的Runable和Ready,线程可能正在执行,也可能正在等待cpu为它分配执行时间。
- Waiting,无限期等待,不会分配执行时间,等待被唤醒:Object.wait()、Thread.join()、LockSupport.park()。
- TimedWaiting,限期等待,过期后系统自动唤醒:Thread.sleep()、Object.wait(time)、Thread.join(time)、LockSupport.parkNanos()、LockSupport.parkUntil()。
- Blocked,阻塞状态,线程等待进入同步区域时状态。
- Terminated,结束状态,线程执行已经结束。
五、ReentrantLock
读写锁
state高16位表示读锁,低16位表示写锁。使用ThreadLocal来记录每个线程的读锁获取次数。
读锁之间共享,可以同时获取锁。写锁与写、读锁都互斥。如果写锁发现任何锁存在都会等待锁释放、读锁发现是写锁则会等待锁释放才能获取锁。
锁降级:
同一个线程拿着写锁,在释放前,使用读锁获取锁,然后释放写锁,随后此线程的读锁获取到锁,则是锁降级。整个过程锁一直在此线程把持,防止其他线程获取到写锁后再做修改而影响读取数据的可见性。
六、java synchronized锁升降级过程