13.2.2 线程安全的实现方法
同步:保障线程安全的一种手段,多线程并发访问共享数据时,保证共享数据在同一时刻只被一条(或一些)线程使用。
1. 互斥同步:互斥是实现同步的一种手段。比如synchronized关键字,Lock接口的实现。synchronized是一个重量级锁,阻塞和唤醒线程涉及到用户态和内核态的切换
2. 非阻塞同步:互斥同步面临的问题是阻塞和唤醒带来的性能的开销。非阻塞采用乐观同步思路,先进行操作,如果操作完毕准备替换时发现此过程中产生竞争,则采用补偿措施例如重试,直到没有竞争为止。这种同步方式不会阻塞线程。
3. 无同步方案:从设计上消除竞争,比如可重入代码,变量线程本地存储。
13.3 锁优化
优化synchronized的加锁策略,减少加锁性能消耗
13.3.1 自旋锁和自适应自旋
在线程获取锁失败时,不是立马进入阻塞,基于锁很快释放的假设,进入一个空转,在这个期间如果获得锁,则省去了阻塞和唤醒的开销,适用锁占用时间很短的情况。缺点是此时会一直占用CPU资源,可以通过配置设置自旋次数,当自旋开启后,默认自旋10次。
自适应自旋,让自旋次数变得智能,如果在一个锁上,经常等待超时,那么会缩短等待次数,相反如果经常很快就能获得锁,那么自动会延长等待次数。
13.3.2 锁消除
虚拟机会对不可能存在共享数据竞争的锁进行消除,判定的依据是逃逸分析技术。
13.3.3 锁粗化
虚拟机检查到一连串零碎的操作都是对同一个对象加锁,会将加锁同步的范围扩大。
13.3.4 轻量级锁
在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。
实现轻量级锁需要了解对象头的内存结构。对象头分为Mark Word,引用类型指针和数组大小三个部分,Mark Word的结构分为:25bit的哈希码,4bit的分代年龄,1bit的偏向模式,2bit的标志位。
其中标志位01-未锁定,00-轻量级锁定,10-重量级锁定,11-GC标记。
1. 轻量级锁加锁过程:
1. 线程访问对象,发现对象标志01尚未加锁,线程在栈空间建立一个锁记录空间,存放对象Mark Word的拷贝,并CAS将对象的Mark Word更新为指向存放在锁空间的备份的指针。
2. 更新成功,则将标志位更新为00。
3. 更新失败,检查指针是否指向自己的锁记录空间,是则直接进入代码块执行
4. 否则说明存在竞争,锁标志位变为10,膨胀为重量级锁。Mark Word中存储的是指向重量级锁(ObjectMonitor对象)的指针,后续尝试加锁的线程会被阻塞
2. 轻量级锁解锁过程:
1. CAS将拷贝替换回去对象头的Mark Word
2. 替换成功,则说明占用期间没有发生竞争
3. 替换失败,则说明锁已经膨胀为重量级锁,将会释放锁,并唤醒被挂起的线程
轻量级锁使用的场景是在加锁期间很少会发生竞争的情况,否则因为存在CAS,性能会比重量级锁还差。
网上有说轻量级锁,在CAS失败时会自旋,有博主通过源码已经验证了是不会自旋的:https://www.jianshu.com/p/d99993b52a07
13.3.5 偏向锁
旨在无竞争的情况下,一个线程占有锁期间内,再次请求锁直接重入而不需要CAS操作。
具体操作是,当对象第一次加锁时,线程将对象的标志位设置为01,将偏向标志设置为1,进入偏向模式。同时使用CAS将本线程的ID记录到对象的Mark Word上的哈希码部分(用了23bit),如果成功,下次同一线程再次进入时,将不需要其他操作。一旦有其他线程尝试加锁,会取消偏向模式。
上面说到占用的内存是对象头的哈希码部分,哈希码一旦生成无法更改,所以一旦有了哈希码之后,将无法在进入偏向模式。并且在偏向模式的状态下,如果有计算哈希码的请求,也将退出偏向模式,进入重量级锁状态,这个时候Mark Word指向ObjectMonitor对象,这其中有字段记录Mark Word,可以将哈希码写入到进入。
很好的介绍偏向锁的博客:https://www.jianshu.com/p/4758852cbff4
13.4 synchronized底层实现
参考:https://www.jianshu.com/p/4e4c67d7ad4a