并发编程中,多个线程共享一个资源时,我们得考虑维护这个资源的原子性,确保
一个线程在操作共享资源时,同时不会被另一个线程所操作
JDK1.6 版本之前,Synchronized被称之为重量级锁,为什么呢?因为Synchronized 是
基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核
态的切换,cpu必须花时间和空间保存当时的操作现场从而增加系统性能开销,在锁竞争
激烈的情况下,Synchronized 同步锁在性能上就表现得很不好。1.6之后,Synchronized
经过多种优化实现了性能的大幅提升,接下来我们就看看Synchronized做了哪些优化,
首先介绍一下Synchronized锁同步的原理
Synchronized同步原理
Synchronized可以用来修饰方法和代码块
修饰代码块的情况下,我们反编译打印出字节码文件看看
Synchronized 在修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。
进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将
释放该Monitor对象
修饰方法时候:
当 Synchronized 修饰同步方法时,并没有发现 monitorenter 和 monitorexit 指令,
而是出现了一个 ACC_SYNCHRONIZED 标志。
流程是这样的:JVM用ACC_SYNCHRONIZED这个标志来判断一个方法是不是同步方法,
当调用了一个方法的时候,会先判断这个方法是否有同步标志ACC_SYNCHRONIZED ,
有的话执行的线程先持有monitor对象,然后开始执行方法,执行完再释放monitor对象。
JVM的同步是基于进入和退出管程Monitor实现的
管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的
软件模块
每个对象都有一个monitor,monitor和对象一起被创建和销毁
Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp实现
一开始,访问同步代码的线程都会被加入到EntryList中,处于block状态,然后去竞争
monitor,Monitor 是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请 Mutex 成功,
则持有该 Mutex,其它线程将无法获取到该 Mutex,如果线程调用wait()方法,那么就会放弃
Mutex,从而进入WaitSet中等待唤醒,因为monitor是依赖cpu实现的,存在操作系统内核态
和用户态之间的切换,有额外的性能开销
Java 对象头
jdk1.6开始,对象实例在jvm堆里面,被分成了三部分,对象头、实例数据和对齐填充,
对象头又由mark word 、指向类的指针和数组长度三部分组成。
mark word记录了对象和锁的相关信息,在64位的jvm中,mark word的长度是64bit
锁升级
Synchronized相较于之前的版本,做的优化便是锁升级,便是依赖markword中锁的信息
而完成锁的升级,由最开始的无锁变成偏向锁、轻量级锁和最后的重量级锁。我们接下来
看看Synchronized是怎么做的
偏向锁
1.6之前,我们获取锁需要进入monitor,释放锁需要退出monitor,其中的系统内核和用户态
的切换便是性能开销所在,我们是想避免这种切换带来的性能开销
在一些场景下,去获取锁的线程可能同一个线程,既然是同一个线程,我们便没必要依赖
monitor而产生性能开销,偏向锁就是去这个对象的mark word里面看偏向锁的线程id是不是
自己,是的话,就不去竞争monitor了,当对象被当作同步锁并且被一个线程持有之后,
锁标志位保持不变,然后把 是否是偏向锁 置为1,然后把线程id记录下来,这样便表示
这个对象此时是一个偏向锁,下次有线程来获取自己的时候,如果还是上次的线程,那么
就直接持有这个锁,如果不是,那么偏向锁就会被撤销,撤销需要等待stw安全点,锁标志
位置为00表示轻量锁,完成锁的一次升级。
轻量级锁
当另一个线程来竞争这个锁的时候,发现是个偏向锁并且mark word记录的线程id不是自己,
就会通过CAS尝试获取这个锁,如果成功,就把线程id改成自己的id,如果失败,那么就升级
为轻量级锁,锁标志位更改为00
自旋锁和重量级锁
另一个线程来竞争锁的时候,线程通过CAS尝试获取锁,假如CAS一次失败后便不再
尝试,从而线程被挂起阻塞,通常情况下,锁的持有时间都不会太久,通过自旋的方式
不断的去尝试获取锁,以避免线程的挂起阻塞,自旋之后还是失败的话,那么锁升级为
重量级锁,锁标志位更改为10,就变成我们最开始说的通过进入和退出monitor竞争锁
多线程调优
当我们已知场景是高并发场景,必然会有大量线程竞争同一资源,那么我们可以关闭偏向锁
,省去等待stw偏向锁撤销的开销,-XX:-UseBiasedLocking
或者直接-XX:+UseHeavyMonitors 设置成为重量级锁
在锁竞争不激烈且锁占有时间不长的情况下,自旋锁可以提高性能,反之,竞争激烈的情况
或者持有时间较长的情况下,自旋更多只是导致大量线程重试占用cpu资源,可以通过
-XX:-UseSpinning关闭自旋锁
JIT编译器通过逃逸分析,如果判断出同步代码使用的对象并不会被发布到其他线程,
那么便不会生成Synchronized的相关机器码,称为锁消除
如果发现多个相邻同步块使用的是同一对象,便会在编译阶段把这几个同步块合并成一个大
的同步块,避免线程反复的申请释放同一个对象
在我们编码层面,我们可以通过锁粒度的减小从而提高并发度,例如ConcurrentHashMap,
从整个数组的粒度变为分段锁的segment粒度,从而提高了并发度