19. 并发终结之Synchronized优化
Synchronized的原理
前面有讲Synchronized的一些知识,这里先做一下总结,当多个线程同时访问一段同步代码时被阻塞了,这些线程会被放到一个Entry Set中,处于阻塞状态的线程都会被放到该列表中;
当线程获取到对象的Monitor时,Monitor是依赖于底层操作系统的Mutex Lock来实现的,线程获取mutex成功,就会持有该mutex,那么其他线程就没法获得mutex。
如果线程调用了wait方法,那么线程就会释放掉mutex,并且进入到wait Set(等待)集合中,等待下一次被其他线程调用notify/notifyAll唤醒。
处于Entry Set和Wait Set的线程均处于阻塞状态,阻塞是由操作系统完成,就存在用户态和内核态之间进行切换。
这里就涉及到Synchronized的优化,即轻量锁自旋。
适应性锁
Synchronized的优化主要包括:锁消除,锁粗化,偏向锁以及适应性锁。这些优化仅在JVM server模式下起作用,且与对象头有关
对象是由对象头,实例数据和对齐补充组成。
对象头里则包括:
1.Mark Word:记录了对象,锁以及垃圾回收相关信息。
1.1无所标记
1.2偏向锁标记
1.3轻量锁标记
1.4重量锁标记
1.5GC标记
2.指向类指针
3.数组长度
对于Synchronized锁来说,锁的升级都是通过Mark word中锁标记位与是否是偏向锁标志位来达成的;Synchronized关键字所对应的锁都是从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成重量级锁。
无锁 -> 偏向锁 ->轻量级锁 -> 重量级锁
-
偏向锁:针对一个线程来说,主要作用是优化同一个线程多次获取一个锁的情况;如果一个Synchronized方法被一个线程访问,那么对象头会在mark word里设置偏向锁标志位,同时还会记录该线程ID;当这个线程再一次访问同一个对象的Synchronized方法时,会检查这个对象的mark word的偏向锁标志和持有线程ID,如果是的话,就不需要锁申请了,直接进入方法体;
当并发量大的情况下,偏向锁会被取消,因为性能开销比较大。 -
轻量级锁(自旋锁spin):如果第一个线程已经获得到该对象的锁,这时候第二个线程也开始尝试获取该对象锁,由于此时第一个线程已经获取到,是偏向锁,第二个线程发现对象mark word标志是偏向锁,但是线程ID并不是自己,那么它会进行CAS操作,来获取锁。
1)如果获取锁成功,那么第二个线程直接替换对象头中的线程ID(但不改变偏向锁标识),避免了用户态到内核态的切换;
2)如果获取锁失败,则表示可能有很多线程在同时争抢这把锁,这时候偏向锁就升级成轻量级锁。 - 重量级锁:如果轻量级锁CAS操作不能获取到锁(一定时间内,因为自旋锁是耗CPU的),这个锁就会升级成重量级锁,进入monitor(内核态),进行阻塞。
锁消除
JIT编译器对内部锁的具体实现做的优化,JIT借助逃逸分析技术来判断同步块所使用的锁对象是否能够被一个线程所访问而没有被发布到其他线程。如果同步块所使用的锁对象只能被一个线程访问,那么编译器不会生成Synchronized表示锁申请和释放的机器码,即不包含monitorenter和monitorexit这两个字节码,消除了锁的使用。常见的又StringBuffer,虽然是线程安全的,但实际应用并不会在多个线程间共享这些类的实例(定义局部变量使用)。
锁粗化
对于相邻的几个同步块,如果这些同步块使用的是同一个锁实例,那么JIT会将这些同步块合并为一个大的同步块,避免了一个线程反复申请和释放同一个锁导致的开销。但是也有负面作用:可能导致一个线程持有锁的时间变长,从而使得同步在该锁智商的其他线程在申请锁时等待时间变长。
如何使用内部锁
- 降低锁争用:减少锁被持有时间和降低锁的申请频率
- 减少临界区长度,可以减少锁被持有的时间,从而降低了锁被争用的概率,减少开销。
- 减少锁的粒度:降低锁的申请频率,拆分不同的锁负责保护不同的共享变量。
- 找替代品:volatile关键字,原子变量,无状态对象,不可变对象,ThreadLocal对象