一概念
-
是Java中一个关键字,是JVM层面提供的同步锁机制,用于保证多线程访问同一资源的可见性、互斥性。即当一个线程已经获取资源锁时,其他试图获取的线程只能等待或者阻塞在那里。
-
访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
代码
反编译
javap -c -s -v -l SynchronizedTest.class
-
synchronized
同步语句块的实现使用的是monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。synchronized 代码块是由一对 monitorenter/monitorexit 指令实现的,上面指令代码中中发现有两个monitorexit,其实后面这个是保证同步代码块抛出异常时锁能得到正确的释放而存在的。
-
在Java6之前,synchronized属于重量级锁,效率低下,因为Monitor 对象监视器锁是同步的基本实现单元,Monitor监视器是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock
实现,完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是为什么早期的synchronized效率低的原因
从Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
7 在Java6中JVM对此进行了大刀阔斧地改进,引入偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能,该优化主要是基于 Mark Word
和Object monitor
来实现的。
二、锁升级
- 锁的升级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级。当没有竞争出现时,默认会使用偏斜锁。JVM 会利用
CAS
操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS
操作 Mark Word
来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
三、锁的概念
1 偏向锁
多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,它的出现是为了解决只有在一个线程执行同步时提高性能。而且在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。那么只需要在锁第一次被拥有的时候,记录下偏向线程ID
。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。如果相等表示偏向锁是偏向于当前线的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
。
偏向锁的实现
:一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word
中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word
中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。
偏向锁的撤销
:偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。
2 轻量级锁
轻量级锁是为了在线程近乎交替执行同步块时提高性能。主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗
,说白了先自旋再阻塞
。升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。此时线程B操作中有两种情况:如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
3 重量级锁
有大量的线程参与锁的竞争,冲突性很高,自旋到达一定次数还是没有获取锁成功,这时候轻量级锁就会膨胀为重量级锁,当锁膨胀为重量锁时,就不能再退回到轻量级锁。
四、总结
synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式。
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁
的升级过程,而不是无论什么情况都使用重量级锁。
偏向锁
:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻量级锁
:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁
:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
锁消除
:同步代码synchronized(o),对象o每次都是new一个新的,那么该锁对象就不会其他线程所使用,此时JIT编译器就会锁消除。
锁粗化
:加入方法中收尾相接,前后相邻的都是同一个锁对象,那个JIT编译器会把这几个synchronized(o)合成一个锁粗化,效果一样。
文章来源