第13章 线程安全与锁优化
1. 线程安全
当多个线程同时访问一个对象时,如果不考虑这些线程的调度和执行,也不需要其他额外的操作,得到的结果都是我们期望的结果,则可以认为这个对象是线程安全的。
哪些对象是线程安全的?
1.1 不可变量 Immutable
被final声明的对象只要在构造的时候没有出现this逃逸这个对象就是线程安全的
a. 对于基本数据类型,定义时声明为 final即可
b. 对于对象数据类型,需要自己保证;如:String、Integer等对象
1.2 局部变量
由于局部变量不可能被多线程访问,所以肯定是线程安全的
2. 如何实现线程安全
2.1 互斥同步
a. synchronized
b. ReentrantLock: 与synchronized 相比,增加了一些高级的功能
1)等待可中断 2)实现了公平锁3)锁可以绑定多个条件
应该如何选择呢?
推荐优先使用synchronized 关键字
原因主要有以下几个:
1)synchronized 语法简单 2)LOCK需要释放锁,需要在finally中释放,synchronized锁的释放是由jvm 来保证的 3)经过十几年的优化,synchronized 的性能差距和LOCK已经变得很小,在绝大部分情况下都在一个可以接受的范围内
2.2 非阻塞同步
借助于硬件指令
1)测试并设置 Test-and-Set
2)获取并增加 Fetch-and-Increase
3)交换 Swap
4)比较并交换 Compare-and-Swap(CAS会有ABA问题产生)
5)加载链接/条件存储 Load-link/Store-Conditional
JDK5 之后在sun.misc.Unsafe 中提供了这些方法,这些方法会被编译成平台相关的指令
3. 锁优化
JDK5 到 JDK6 花费了大量的资源去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking) 等
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁,随着锁的竞争,可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)
轻量锁加锁过程如下:
1)检查同步对象当前有没有锁,如果锁状态为 01 (表示没有),在当前的线程中建立一个名为 Lock Record 的空间,把同步对象的 Mark Word 拷贝过来
2)虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向 Object Mark word,如果成功则执行步骤(3),否则执行步骤(4)。
3)如果这个动作执行成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为”00“,表示对象处于轻量锁的状态;
4)如果这个操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则就说明这个对象已经被其他线程占用了,如果出现两个以上的线程争用同一个锁的情况,轻量级锁就会膨胀为重量级锁,锁标志的状态值变为”10“, Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
3.2 偏向锁
偏向锁中的偏,指的是偏心的意思,它会偏向于第一个获得它的线程
锁对象第一次被线程获取时,虚拟机会将对象头中的标志位设置为01 把偏向模式设置为 1 ——表示进入偏向状态,同时把获取这个锁的线程的ID记录在Mark Word中
如果在接下来的执行过程中,持有偏向锁的线程以后每次进入到这个锁的同步块时,虚拟机都可以不再进行任何同步操作。
一旦出现另一个线程去尝试获取这个锁的情况,偏向模式马上宣告结束。