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