8 锁与并发

来源:互联网 发布:500px哪些摄影师 知乎 编辑:程序博客网 时间:2024/05/29 17:11

8 锁与并发

对象头与锁

在java虚拟机的实现中,每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个称为Mark Word的部分,它是实现锁的关键。

在32位系统中,Mark Word为一个32位的数据,在64位系统中,它占64位。它是一个多功能的数据区,可以存放对象的hash值,对象年龄,锁的指针信息。一个对象是否占用锁,占用哪个锁,就记录在这个Mark Word中。

以32位系统为例,普通对象的对象头如下显示。它表示Mark Word中有25位比特表示对象的hash值,4位表示对象的年龄,1位比特表示它是否为偏向锁,2位比特表示锁的信息。

对于偏向锁的对象,它的格式如下:
前23位表示持有偏向锁的线程,后续2位比特表示偏向锁的时间戳(epoch),4位比特表示对象的年龄,年龄后1位比特固定为1,表示偏向锁,最后2位为01表示可偏向/未锁定。

当对象处于轻量级锁定时,Mark Word最后2位为00,处于重量级锁定时,后两位为10,普通未锁定情况下,后三位为001.

偏向锁

其核心思想是如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省操作时间。如果在此之间有其他线程进行了锁请求,则锁退出偏向模式。在JVM中,使用-XX:+UseBiasedLocking可以设置启用偏向锁。

当锁处于偏向模式时,对象头会记录获得锁的线程

[javaThread* | epoch | age | 1 | 01]

这样,当该线程再次尝试获得锁时,通过Mark Word的线程信息就可以判断当前线程是否持有偏向锁。

使用参数-XX:BiasedLockingStartupDelay=0 设置虚拟机在启动后立即使用偏向锁,否则默认值为4.

在竞争激烈的场合,可以尝试禁用偏向锁来提高性能。

轻量级锁

如果偏向锁失败,java虚拟机会让线程申请轻量级锁。轻量级锁在虚拟机内部,使用一个称为BasicObjectLock的对象实现,这个对象内部由一个BasicLock对象和一个持有该锁的java对象指针组成。BasicObjectLock对象放置在java栈帧中。在BasicLock对象内部还维护着displaced_header字段,它用于备份对象头部的Mark Word.

当一个线程持有一个对象的锁时,对象头部Mark Word如下所示:

[ptr | 00 ] locked

末尾两位比特00,整个Mark Word为指向BasicLock对象的指针。由于BasicObjectLock对象在线程栈中,也只需要简单的判断对象头的指针是否在当前线程的栈地址范围内即可。同时,BasicLock对象的displaced_header字段,备份了原对象的Mark Word内容。BasicObjectLock对象的obj字段则指向该对象。

首先,BasicLock通过set_displaced_header()方法备份了原对象的Mark Word。接着,使用CAS操作,尝试将BasicLock的地址复制到对象的头的Mark Word。如果复制成功,那么加锁成功,否则认为加锁失败。如果加锁失败,那么轻量级锁可能膨胀为重量级锁。

锁膨胀

当轻量级锁失败,虚拟机就会使用重量级锁,在使用重量级锁时,对象的Mark Word如下:

[ptr | 10] monitor

末尾2比特标记为10,整个Mark Word表示指向monitor对象的指针。

启用过程分为两步,首先,通过inflate()方法进行膨胀,为的是获取对象的ObjectMonitor,然后使用enter()方法尝试进入该锁。

自旋锁

进入ObjectMonitor的enter(),线程很可能会在操作系统层面被挂起,这样线程上下文切换的性能损失比较大。因此,锁膨胀后,虚拟机会进行最后的争取,希望线程可以尽快进入临界区而避免被操作系统挂起。

自旋锁可以使线程在没有取得锁时,不被挂起,而转而去执行一个空循环,在若干个空循环后,如果可以获的锁,则继续执行。若线程依然得不得锁,才会被挂起。

锁消除

基于逃逸分析技术,虚拟机可以捕获到这些不可能存在竞争却申请锁的代码段,并消除这些不必要的锁,从而提高系统性能。

使用锁消除必须开启 -XX:+DoEscapeAnalysis 和 -XX:EliminateLocks,并且必须工作在-server模式下。

锁在应用层的优化思路

减少锁持有时间

只对必要的代码加锁

减小锁粒度

典型的使用场景是ConcurrentHashMap的实现。ConcurrentHashMap将整个HashMap分为若干个段,每个段都是一个子hashmap.

如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放在哪个段中,然后对该段加锁,完成put()操作。只要被加入的表项不在同一个段中,则线程便可以做到真正的并行。

但是,当系统需要全局锁时,其消耗的资源比较多,比如ConcurrentHashMap的size()方法。

锁分离

锁分离是减小锁粒度的一个特例,它根据应用程序的功能特点,将一个独占锁分成多个锁。一个典型的案例就是java.util.concurrent.LinkedBlockingQueue的实现。

在LinkedBlockingQueue的实现中,take()和put()函数分别实现了从队列中取得数据和往队列中增加数据的功能。

在jdk的实现中,使用两把锁分离了take()和put()操作。

锁粗化

把对同一把锁的连续请求整合成一次请求。

理解java内存模型

原子性
有序性
可见性