前言

循序渐进,阅读本文之前,建议先阅读:

java高端基础:可见性、原子性、有序性(并发bug的源头)

Java高端基础:线程的生命周期

java高端基础:Thread源码解读

java高端基础:ThreadLocal的源码解读

为什么要优化

DK6之前,synchronized是重量级锁,加锁和解锁都需要依赖于操作系统底层的Mutex Lock来实现,会涉及到从用户态转换成内核态,这种转换成本比较高。

基础知识

对象的锁的状态会存在对象头中,(对象头中除了锁状态之外,还有HashCode,GC分代年龄等);

锁状态通过锁的标志位来判断,01 标识可偏向状态,但具体是不是偏向锁需要通过其他位来判断,有一个位置专门记录是否位偏向锁,0标识无锁,1表示偏向锁。00表示轻量级锁,10表示重量级锁。

轻量级锁

加锁过程如下:首先在线程栈帧中创建一个存储锁记录的空间,然后将对象头信息拷贝到这个空间中;然后使用CAS的方式,尝试将对象的对象头中的一个指针指向到锁记录空间,同时锁记录空间的一个owner字段会指向对象头。如果指向成功,则轻量级锁获取成功,锁标志位变为00。

解锁过程如下,通过CAS操作,尝试用锁记录空间中的对象替换当前对象的对象头。如果替换成功,则整个同步过程完成。如果替换失败,说明有其他线程尝试获取该锁,要在释放锁的同时,唤醒被挂起的线程,这时候也就升级为重量级锁。

从轻量级锁的加解锁过程来看,如果不存在多线程同时争抢一把锁,则不需要在用户态和核心态之间切换。

偏向锁

轻量级锁的加解锁操作需要CAS操作,如果一直是同一个线程来获取锁,那么便有更好的方式来实现,即偏向锁。偏向锁顾名思义就是偏向于某个线程(实际上是偏向于第一个获取锁的线程);

加锁过程:首先判断标志位是否为可偏向状态,如果是可偏向状态,则判断线程ID是否是自身线程的ID,如果是则执行同步代码,如果不是则通过CAS操作竞争锁(也有可能是第一个线程第一次获取锁,没有竞争,但依然需要通过CAS方式加锁,后面该线程再获取锁将不在需要CAS操作,这是对轻量级锁优化),竞争成功则将线程ID设置为自身ID,否则表示有竞争,那么在全局安全点时(全局安全点指没有字节码正在执行的时间点),偏向锁升级为轻量级锁。

解锁过程:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,否则线程不会主动去释放偏向锁。释放过程即上面提到的升级为轻量级锁的过程。




java 得到当前线程的ID_java 获取当前线程id

内存模型



自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

关于自旋时间的选择

jdk1.5这个限度是写死的,在1.6引入了适应性自旋锁,它是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化:

如果平均负载小于CPU数则一直自旋

如果有超过(CPU数/2)个线程正在自旋,则后来线程直接阻塞

如果CPU处于节电模式则停止自旋

如果正在自旋的线程发现了进入临界区的线程变化则延迟自旋时间(自旋计数)或进入阻塞

自旋时会适当放弃线程优先级之间的差异

自旋锁的应用:在重量级锁竞争的时候,会使用到,一定程度上避免线程进入锁等待队列(因为一旦进入锁等待队列,就是一个阻塞状态,而阻塞状态的唤醒需要内核态与用户态进行切换,比较耗资源),因为这样所以synchronized是非公平锁。重量级锁简图:




java 得到当前线程的ID_用户态_02

重量级锁简图



另外在CAS中也用到了自旋。

最后放一张比较详细的图(网上拷贝,并不保证图中没有错误)




java 得到当前线程的ID_java 得到当前线程的ID_03

锁优化