在Java(1.6+)中锁的状态一共有四种,级别由低到高分别是:无锁、偏向锁、轻量级锁、重量级锁,这几个状态会随着竞争情况逐渐升级,其中锁可以升级,但是不能降级。Java中加锁的最简单方式就是加synchronized关键字,那么为什么锁会有这么多状态的锁呢?
在Java早期,synchronized叫做重量级锁,加锁过程需要操作系统在内核态访问核心资源,因此操作系统会在用户态与内核态之间切换,效率很低下。于是JDK1.6之后,JVM为了提高锁的获取与释放效率,对synchronized进行了优化,引入了偏向锁和轻量级锁,根据线程竞争情况对锁进行升级,在线程竞争不激烈的情况避免使用重量级锁。
对象头
在了解锁之前需要先了解一下对象头,我们都知道在Java中锁不是某一个具体的实物资源,而是对象上的某个标记,而这个标记就记录在对象头上。
Mark Word(对象头)是Java对象布局中的一个部分,关于Java对象布局可以参考我的另一篇博客:《Java对象的内存布局》,那么Mark Word内部是什么样子的呢?
在32位虚拟机中:
在64位虚拟机中:
由于现在计算机基本都是64位,所以下面以64位虚拟机为例,看一下锁具体是如何升级的
无锁:对象头中有31bit的空间来存储对象的hashcode,4bit用于存放对象分代年龄,1bit来表示是否是偏向锁,2bit存放锁标志位,偏向锁位与锁标志位合起来“001”就代表无锁。无锁就是没有对任何资源进行锁定,所有线程都能访问并修改资源。
偏向锁:对象头中记录了获得偏向锁的线程ID,偏向锁与锁标志位合起来“101”就代表偏向锁。有研究发现,在大多数情况下,锁很少被多个线程同时竞争,而且总是由同一个线程多次获得,因此只需要将获得锁的线程ID写入到锁对象Mark Word中,相当于告诉其他线程,这块资源已经被我占了。当线程访问资源结束后,不会主动释放偏向锁,当线程再次需要访问资源时,JVM就会通过Mark Word中记录的线程ID判断是否是当前线程,如果是,则继续访问资源。所以,在没有其他线程参与竞争时,锁就一直偏向被当前线程持有,当前线程就可以一直占用资源或者执行代码。
自旋锁:一旦有另外一个线程参与锁竞争,偏向锁就会升级为自旋锁,此时撤销偏向锁,锁标志位变为“00”。竞争的两个线程都在各自的线程栈帧中生成一个Lock Record空间,用于存储锁对象目前Mark Word的拷贝,用CAS操作将Mark Word设置为指向自己这个线程的LR(Lock Record)指针,设置成功者获得锁,其他参与竞争的线程如果未获取到锁,则会一直处于自旋等待的状态,直到竞争到锁。
重量级锁:长时间的自旋操作是很消耗CPU资源的,为了避免这种盲目的消耗,JVM会在有线程超过10次自旋,或者自旋次数超过CPU核数的一半(JDK1.6以后加入了自适应自旋-Adaptive Self Spinning,由JVM自己控制自旋次数)时,会升级到重量级锁。重量级锁底层是依赖操作系统的mutex互斥锁,也就是有操作系统来负责线程间的调度。重量级锁减少了自旋锁带来的CPU消耗,但是由于操作系统调度线程带来的线程阻塞会使程序响应速度变慢。