探讨java中synchronized关键字的工作机制
背景
synchronized是java语言中用来实现同步机制最常用的关键字,有很多文章都已经将它的用法说的很清晰了,本文只探讨synchronized的底层是怎么实现加锁机制的。
在JDK1.5以前,synchronized关键字在JVM中主要通过mutex(互斥锁)实现,这种锁又称重量级锁,每当有其他线程占用锁后,其他想要获得此锁的线程就得处于阻塞状态,而线程的调度是比较耗费CPU资源的,因此这种锁称为是重量级锁。
synchronized和ReentrantLock的比较
而此时基于Java语言编写的ReentrantLock性能要高于synchronized,ReentrantLock采用独占共享模式的,基于AQS(AbstractQueuedSynchronizer )实现的,在AQS内部,维护一个FIFO的多线程队列,每个要获得此锁的线程都要进入到这个队列,如果已经有现成占用了此锁,那么新来的线程需要通过自旋的形式去尝试获得该锁,经过有限的尝试后,仍未获取成功,线程则会被阻塞住,等待前面的线程释放锁后唤醒。因此java.util.concurrent包中很多类是通过ReentrantLock加锁的。
synchronized机制
JDK1.6以后,优化了synchronized的工作机制,实现了三种类型的加锁机制,分别是:偏向锁、轻量级锁和重量级锁,一般来说一开始默认是加偏向锁,它们会随着竞争的激烈而逐渐升级,并且只会升级不会降级。
synchronized实现这三种机制是通过对象头来实现的。
对象大家都很熟悉,但大家可能没注意过对象头吧,对象头中分为三部分,分别是:Mark World、Class Point、数组长度。
和加锁机制有关的部分是 Mark World,里面的主要内容有:加锁状态、HashCode。
咦,这里面怎么会有HashCode,HashCode有必要存储吗?每次用的时候单独计算一下不行吗?不行,我们知道一个对象的HashCode值是不变的,我们也确实需要它不变,如果在程序的运行过程中HashCode值变来变去,岂不是有很大的bug? 而Object中的HashCode值是根据对象的内存地址计算的,但是经过GC后,对象在内存中的地址可能变化,如果此时再对HashCode值进行计算,则肯定和之前的值不一样,因此我们要将第一次计算出的HashCode值进行存储,之后需要HashCode值则直接从对象头中读出,不过这不是我们要讨论的关键,我们要讨论的是加锁状态。
加锁状态可分为:无锁状态、偏向锁状态、轻量级加锁状态、重量级枷锁状态、GC标记(已经被加锁但还未释放锁的对象不存在被GC)
其中偏向锁的获取和释放对性能影响最小,因为此时只有一个线程(线程1)去尝试获取该锁,如果此时有其他的线程(线程2)也来尝试获取此锁,则线程2发现有其他线程已经占用过此锁,则线程2需要通过自旋的方式竞争此锁,此时该锁升级为轻量级锁。当很多线程去竞争同一把锁时,没竞争到锁的线程会处于阻塞状态,此时CPU调度线程的开销会更大,锁会升级为重量级锁。