首先来说下synchronize和Lock的区别:
两者都是锁,用来控制并发冲突,区别在于Lock是个接口,提供的功能更加丰富,除了这个外,他们还有如下区别:
- synchronize自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronize释放锁是由JVM自动执行的。
- Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入)
- Lock可以让线程在获取锁的过程中响应中断,而synchronize不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。
- Lock锁的是代码块,synchronize还能锁方法和类。
Lock可以知道线程有没有拿到锁,而synchronize不能(Thread.holdsLock方法可以让synchronize也能感应到是否获取到了锁)
Lock锁对应有源码的,可以查看下代码,那么synchronize在JVM层面是怎么实现的呢,我们看下字节码文件:
- 先用javac Test.class 编译出class文件
- 再用javap –c Test.class查看字节码文件
我们写个DEMO看下,JVM底层是怎么实现synchronized的:、
public class Test4 { private static Object LOCK = new Object(); public static int main(String[] args) { synchronized (LOCK){ System.out.println("Hello World"); } return 1; } }
在看下上面代码对应的字节码
也就是说,锁是通过monitorenter和monitorexit来实现的,这两个字节码代表的是啥意思:
可以在下面参考的网页中了解monitorenter和monitorexit的作用,我就不盗用他们的话了,大致意思是,每个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的。
我之前分析过一篇ReenternLock,概念都是类似的,只是锁是自身维护了一个volatile int类型的变量,通过对它加一减一表示占有锁啊重入之类的概念。
注意,如果synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增加这个一个标识符,获取它的monitor,所以本质上是一样的。
HotSpot中锁的具体实现以及对它的优化:
重量级锁:
最基础的实现方式,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程。阻塞和唤醒操作是依赖操作系统来完成的,所以需要从用户态切换到内核态,开销很大。并且monitor调用的是操作系统底层的互斥量(mutex),本身也有用户态和内核态的切换,所以JVM引入了自旋的概念,减少上面说的线程切换的成本。
自旋锁:
如果锁被其他线程占用的时间很短,那么其他获取锁的线程只要稍微等一下就好了,没必要进行用户态和内核态之间的切换,等的状态就叫自旋。例如如下代码:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS,获取值不对则无限循环
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
自旋会跑一些无用的CPU指令,所以会浪费处理器时间,如果锁被其他线程占用的时间段的话确实是合适的…如果长的话就不如使用直接阻塞了,那么JVM怎么知道锁被占用的时间到底是长还是短呢?
因为JVM不知道锁被占用的时间长短,所以使用的是自适应自旋。就是线程空循环的次数时会动态调整的。
可以看出,自旋会导致不公平锁,不一定等待时间最长的线程会最先获取锁。
轻量级锁:
JDK1.6之后加入,它的目的并不是为了替换前面的重量级锁,而是在实际没有锁竞争的情况下,将申请互斥量这步也省掉。锁实现的核心在与对象头(MarkWord)的结构,对象自身会有信息表示所有被锁住并且锁是什么类型,如下所示:
如果代码进入同步块时,检测到对象未锁定,即标志位为01。那么当前线程就会在自身栈帧中建议一个区域保存对象的MarkWord信息,再使用CAS的方式让这个区域指向对象的MarkWork区域,这样就算加上锁了。(这样就没有获取系统mutex变量,只是改了个值,但是如果有竞争的话,就要升级成重量级锁,这样反倒变慢了)
加锁前VS 加锁后:
偏向锁:
比轻量级锁更绝,将同步操作全部省略…设置步骤是和前面的轻量级锁一样的,不同的是标志位设置的是01,即偏向模式。
不同的是同一个线程第二次进来之后,虚拟机不会再进行任何的同步操作,比如Mark Word的update。
如果有其他线程来,偏向模式就结束了,标志位会恢复到未锁定或者偏向锁。所以如果锁总是会被多个线程访问的话,还是禁止掉偏向锁优化比较好。
锁优化流程如下:(出自周志明老师的那本讲解JVM的书)
可以看出,锁是一个逐步升级的过程,不会一开始上来就重量级锁。锁一般只会升级不会降级,避免降级之后冲突导致效率不行并且又得升级。但是降级其实是允许的(STW的时候),可以看下参考中文章里面提到的英文网站。
其他的优化还有锁消除以及锁粗化:
如果一段代码其实在作用域可以不加锁的,Javac编译器会自动优化。
锁粗化是指代码在一段代码中多次加锁,会被JVM优化成对整个代码段加锁。
(但是这两点是JVM对代码的优化,而不是对synchronized优化了,这里只是顺带提一下)
参考:
https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-3.html#jvms-3.14 (JVM规范官方文档)
https://www.cnblogs.com/lycroseup/p/7486860.html(monotorenter/exit描述)
https://blog.csdn.net/qq_34337272/article/details/81252853(CAS中的自旋锁)
https://www.jianshu.com/p/36eedeb3f912(锁选用流程图)
https://blog.csdn.net/kirito_j/article/details/79201213(自旋锁的那两张图片)
https://www.jianshu.com/p/9932047a89be(JVM锁降级)