ReentrantLock——重入锁
ReentrantLock的在AQS与简介源码分析中有所提及,本篇将用JDK1.8深入探索它内在的含义以及公平锁和非公平锁的性能测试比较。
ReentrantLock实现了锁接口,锁接口提供比使用同步方法和语句有着更灵活的结构化,此外它还提供了其他功能,包括定时的锁等待,可中断的锁等待并且可以支持多个关联条件对象关于条件后续博文会单独解释)。
所有Lock的实现类都必须提供与内部锁相同的内存语义,与synchronized相同的互斥性和内存可见性:
如图1所示,成功的锁定操作与成功的锁定操作具有相同的内存同步效果。
2,成功的解锁操作与成功的解锁操作具有相同的内存同步效果。
3,不成功的锁定和解锁操作以及重入锁定/解锁操作不需要任何内存同步效果
使用模版方式如下:
锁定中提供了如下方法
的ReentrantLock只提供独占式获取同步状态的操作,提供了获取锁时的公平和非公平选择,它的构造函数中提供了两种公平性选择,默认的构造函数是创建非公平锁(参见下图) 。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程持有非公平的锁时,在执行相关逻辑后释放了锁,这个时候该锁的状态变为可用,那么这个线程可能跳过队列中所有的等待线程并获得该锁,这也是公平锁和非公平锁实质的区别。
非公平锁的获取
1,Sync为ReentrantLock里面的一个内部类,它继承AQS,有两个子类:公平锁FairSync和非公平锁NonfairSync。
2,通过cas获取同步状态,将同步状态设置为1,这需要当前线程设置成为独占式线程的原因是给锁可以重入做铺垫的用的。
3,如果没有获取到锁,则调用AQS的获取(1)方法
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))这个方法如果有不清楚的请参考AQS简介与源码剖析这里调用ReentrantLock的tryAcquire方法近而调用nonfairTryAcquire获取同步状态
这里主要做了两件事:
如图1所示,若线程状态= 0的状态,尝试获取同步状态成功则将当前线程设置成独占式线程。
2,若状态!= 0,判断当前线程是否为独占式线程,如果是则说明锁重入了,线程的同步状态值的状态增加
非公平锁的释放
调用AQS的release方法,如果释放锁成功则唤醒后继节点尝试获取同步状态,来着重看下tryRelease(int releases)
tryRelease的方法如下几件事:
1,获取当前线程的同步状态值的状态减去的版本中,赋值给℃。
2,如果当前线程不等于独占式线程则抛出llegalMonitorStateException异常
3,如果c等于0,说明没有线程持有该锁了,这个时候需要将独占式线程显示的设置为null,锁释放成功。
如图4所示,如果!C = 0,则设置同步状态值的状态为C,这个时候锁没有释放成功。
这里需要说明的是:
如果这个锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为空(如果不将占有线程设置为空这个时候锁还可以重入)并返回真,表示释放成功。
公平锁的获取
公平锁的获取流程按下图从上而下:
的tryAcquire的解释可以参考AQS简介与源码分析,与nonfairTryAcquire比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
公平锁的释放
公平锁和非公平锁的释放都是一套代码一样的逻辑
公平锁与非公平锁性能测试
public class FairAndUnFair {
private static Lock fairLock = new ReentrantLock2(true);
private static Lock unfairLock = new ReentrantLock2(false);
私人 空隙 testLock(锁锁) {
对于(诠释 I = 0 ; I <= 4 ;我++){
线程线程= 新线程(新作业(锁定)){
公共字符串的toString() {
return getName();
}
};
thread.setName(“ thread- ” + i);
thread.start();
}
}
私人 静态 类 作业 扩展 线程 {
私人锁定锁 ;
公共 职务(锁定锁定) {
this。锁定 = 锁定 ;
}
public void run() {
for(int i = 0 ; i <= 1 ; i ++){
lock。lock();
尝试 {
String currentThreadname = Thread.currentThread()。getName();
Collection <Thread> queuedThreads =((ReentrantLock2)lock).getQueuedThreads();
系统。out .println(“Lock by” + currentThreadname + “,等待” + queuedThreads);
} 捕获(例外五){
e.printStackTrace();
} 终于 {
锁。开锁();
}
}
}
}
private static class ReentrantLock2 extends ReentrantLock {
public ReentrantLock2(boolean fair) {
super(fair); }
}
public Collection <Thread> getQueuedThreads() {
List <Thread> arrayList = new ArrayList <Thread>(super。getQueuedThreads
());
Collections.reverse(ArrayList的);
返回 arrayList;
}
}
@ Test
public void fair() {
testLock(fairLock);
}
@ 测试
public void void unfair()抛出BrokenBarrierException,InterruptedException {
testLock(unfairLock);
}
}
经过的JUnit测试结果如下
非公平锁:
公平锁:
对比上面的结果:
公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。
原因是:非公平锁只要线程获取同步状态就表示成功获取到锁,如果线程刚释放了锁,那么这个时候会很可能再次获取同步状态,使得其他线程只能在同步队列中等待。
下面运行测试用例
测试环境:
测试场景:
10个线程,每个线程获取100000次锁,通过vmstat的统计测试运行时系统线程上下文切换的次数
vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存,进程,IO读写,CPU活动,每秒上下文切换等进行监视。
public class FairAndUnfairLockTest {
private static Lock lock = new ReentrantLock2(true);
私人 静态 类 作业 实现 Runnable {
私人锁定锁 ;
私人 CyclicBarrier cyclicBarrier;
公共 作业(锁定锁,CyclicBarrier cyclicBarrier) {
this。锁定 = 锁定 ;
这个 .cyclicBarrier = cyclicBarrier;
}
public void run() {
for(int i = 0 ; i < 100000 ; i ++){
lock。lock();
尝试 {
系统。out .println(i + “获取锁的当前线程[” + Thread.currentThread()。getName()+ “],同步队列中的线程” +((ReentrantLock2)lock).getQueuedThreads());
} finally {
//一定要记得释放锁
lock .unlock();
}
}
try {
// CyclicBarrier理解成栅栏直到十个线程都达到这里然后打开栅栏放行
cyclicBarrier。等待();
} catch(InterruptedException e){
e.printStackTrace();
} catch(BrokenBarrierException e){
e.printStackTrace();
}
}
}
/ **
*获取同步队列中的线程
* /
private static class ReentrantLock2 extends ReentrantLock {
public ReentrantLock2(boolean fair) {
super(fair); }
}
//列表是逆序输出,为了方便观察结果,将其进行反转
public Collection <Thread> getQueuedThreads() {
List <Thread> arrayList = new ArrayList <Thread>(super。getQueuedThreads
());
Collections.reverse(ArrayList的);
返回 arrayList;
}
}
/ **
*十个线程运行结束后计算执行时长
* /
private static class CountTime implements Runnable {
//公平锁,非公平锁
private String lockMode;
//记录的开始时间
private long beginTime;
public CountTime(String lockMode,long beginTime) {
this.beginTime = beginTime;
这个 .lockMode = lockMode;
}
public void run() {
系统。out .println(lockMode + “执行时长:” + String.valueOf(System.currentTimeMillis() - beginTime));
}
}
public static void main(String [] args) {
//公平锁
字符串lockMode = “公平锁定” ; }
long beginTime = System.currentTimeMillis();
// 10个线程执行完成后,再执行CountTime线程统计执行时间
CyclicBarrier cyclicBarrier = new CyclicBarrier(10,new CountTime(lockMode,beginTime));
for(int i = 0 ; i <10 ; i ++){
Thread thread = new Thread(new Job(lock,cyclicBarrier)){
//如果不覆写toString的话线程名看着不太清晰
public String toString() {
return getName();
}
};
thread.setName(“ thread- ” + i);
thread.start();
}
}
}
公平锁执行结果:
非公平锁执行结果:
CS表示:每秒上下文切换数
vmstat 1:每1秒采集数据一次
在测试中公平性锁与非公平性锁相比,总耗时是其2.1倍。可以看出,公平性锁保证了锁的获取按照先进先出原则,而代价是进行大量的线程切换。非公平性虽锁然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
参考:
Doug Lea:“Java并发编程实战”
方腾飞,魏鹏,程晓明:“Java并发编程的艺术”
****文章同步会慢些,欢迎关注微信公众号:挨踢男孩