ReentrantLock——重入锁

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并发编程的艺术》


猜你喜欢

转载自blog.csdn.net/rui920418/article/details/80497818