浅析Java并发之锁机制
线程安全
锁在并发编程经常会使用到,主要是用来保护临界区资源不会因为被多个线程同时访问而受到破坏,如果多个线程对临界区资源进行访问的话,可能会导致数据的不一致性,程序运行会得到错误的结果。
通过锁的话,我们可以实现线程安全,即并发环境下,多个线程对对象进行访问,对象的状态始终保持一致,并且能与在单线程环境下运行得到相同的结果,即线程的行为是正确的。
对象头
在Java虚拟机中(特指Hotspot),对象在内存中存储的布局主要分为三个部分对象头,实例数据和对齐填充。
在这里我们主要讲一下对象头,对象头包括两个部分对象运行时数据和类型指针,跟锁相关的主要运行时数据这一块。
运行时数据主要包括hashcode、Gc分代年龄、锁状态持有的锁、偏向线程ID、偏向时间戳等。这部分被称为Mark Word.Mark Work是一个非固定的数据结构,它能根据对象的状态对存储空间进行复用。
- 在32位系统中,普通对象的对象头如下
1 | hashcode : 25 | age : 4 | biased_lock : 1 | lock : 2 |
表示在Mark Word中有25位比特用来表明对象的哈希值,4位比特表示对象的分代年龄,1位比特表示是否为偏向锁,2位比特表示锁的信息。
- 偏向锁
对于偏向锁的对象
1 | JavaThread* : 23 | epoch : 2 | age : 4 | biased_lock :1 | 01 |
前23位比特表示持有锁的线程,后两位比特表示偏向锁的时间戳(epoch),4位比特表示对象年龄,年龄后一位比特固定为1,表示偏向锁,最后2位表示可偏向/未锁定。
- 轻量级锁
1 | | ptr : 30 | 00 | locked |
当对象被轻量级锁锁定的时候,ptr指向获得锁的线程栈中该对象的真实对象头,最后2位比特为00.
- 重量级锁
1 | |ptr : 30 | 01 | monitor |
当对象被重量级锁锁定的时候,ptr指向的是Monitor,最后两位比特是01
- 未锁定
1 | hashcode : 25 | age : 4 | biased_lock : 0 | 01 |
前29位表示对象的哈希值和年龄,倒数第三位为0,最后两位为01,表示未锁定,虚拟机通过倒数第三位来判断是否为偏向锁。
锁机制
偏向锁
偏向锁表示如果程序没有竞争,就取消之前已经取得锁读的线程同步操作,即某一锁被线程获取后,进入偏向模式,当线程再次请求这个锁的时候,就不需要再进行相关的同步操作,节省时间。如果在线程持有锁期间,有其他线程进行锁请求,则锁退出偏向模式。
在JVM使用-XX:+UseBiasedLocking可以设置启动偏向锁。当锁对象处于偏向模式时,对象头会记录获得锁的线程。
轻量级锁
如果偏向锁失败,虚拟机会让线程去申请轻量级锁。轻量级锁在虚拟机中是使用BasicObjectLock的对象实现。对象内部持有BasicLock对象和一个持有该锁的对象指针。BasicObjectLock对象存储在Java虚拟机栈的栈帧中,在BasicLock对象内部维护displaced_header用来备份对象的Mark Word,obj字段指向轻量级锁对象
当对象被轻量级锁锁定的时候,Mark Word最后两位比特为0,整个Mark Word为执行BasicLock对象的指针,由于BasicObjectLock对象在虚拟机栈中,因此指针必然指向轻量级锁所在的虚拟机栈空间,即我们只需要判断某一个线程是否持有该对象锁时,只需要判断该对象头的指针是否在当前线程的栈地址范围内就可以了。
轻量级加锁操作首先由BasicLock通过备份原对象的Mark Word,然后通过cas操作,尝试将BasicLock复制到对象头的Mark Word,如果复制成功,则加锁成功,否则加锁失败,那么轻量级锁可能会膨胀为重量级锁。
重量级锁
当轻量级锁失败的时候,虚拟机就有可能使用重量级锁,对象头的Mark Word的最后两位比特会被设置为01,然后Mark Work会指向monitor对象的指针。
轻量级锁失败后,启用重量级锁的时候会废弃BasicLock备份的对象头信息,然后正式启用重量级锁,首先获取对象的ObjectMonitor,然后试图去进入该锁。在试图进入临界区的时候,线程可能会被挂起。
自旋锁
锁膨胀的时候,线程有可能会被挂起,虚拟机为了线程能够尽快进入临界区而不是被挂起,虚拟机会使用自旋锁。
自旋锁可以让线程在没有得到锁的时候,不被挂起,而是去执行一个空循环,在执行若干的循环后,如果线程获得锁,则继续执行,否则的话,线程就会被挂起。
自旋锁对于那些竞争不怎么激烈,锁占用时间短的并发线程,可以减少挂起几率,保证线程执行的连贯,否则对于那些锁竞争激烈,锁占用时间长的并发程序,自旋锁在自旋等待之后仍然没有获得锁的话,浪费了CPU时间,最后还是会被挂起,浪费了系统资源。
在JDK1.6的时候,可以通过-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。
在JDK1.7的时候,虚拟机取消了自旋锁的参数设置,虚拟机会自行调整自旋锁和等待次数。
锁消除
锁清除是指虚拟机在JIT编译的时候,通过对运行上下文的扫描,基于逃逸分析技术,去除不可能存在共享资源竞争的锁,可以通过锁清除,减少请求锁的时间。
逃逸分析和锁消除可以通过-XX:+DoEscapeAnalysis和-XX:+EliminateLocks开启,不过锁消除必须是使用在-server模式下。