前言

为了节省各位的时间,我简单介绍一下这篇文章。这篇文章主要分为三块:Lock的实现,AQS的由来(通过演变的方式),JUC三大工具类的使用与原理剖析。

  • Lock的实现:简单介绍ReentrantLock,ReentrantReadWriteLock两种JUC下经典Lock的实现,并通过手写简化版的ReentrantLock和ReentrantReadWriteLock,从而了解其实现原理。

  • AQS的由来:通过对两个简化版Lock的多次迭代,从而获得AQS。并且最终的Lock实现了J.U.C下Lock接口,既可以使用我们演变出来的AQS,也可以对接JUC下的AQS。这样一方面可以帮助大家理解AQS,另一方面大家可以从中了解,如何利用AQS实现自定义Lock。而这儿,对后续JUC下的三大Lock工具的理解有非常大的帮助。

  • JUC三大工具:经过前两个部分的学习,这个部分不要太easy。可以很容易地理解CountDownLatch,Semaphore,CyclicBarrier的内部运行及实现原理。

不过,由于这三块内容较多,所以我将它拆分为三篇子文章进行论述。

一,介绍

Lock

Lock接口位于J.U.C下locks包内,其定义了Lock应该具备的方法。

Lock 方法签名:

  • void lock():获取锁(不死不休,拿不到就一直等)
  • boolean tryLock():获取锁(浅尝辄止,拿不到就算了)
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:获取锁(过时不候,在一定时间内拿不到锁,就算了)
  • void lockInterruptibly() throws InterruptedException:获取锁(任人摆布,xxx)
  • void unlock():释放锁
  • Condition newCondition():获得Condition对象

ReentrantLock

简介

ReentrantLock是一个可重入锁,一个悲观锁,默认是非公平锁(但是可以通过Constructor设置为公平锁)。

Lock应用

ReentrantLock通过构造方法获得lock对象。利用lock.lock()方法对当前线程进行加锁操作,利用lock.unlock()方法对当前线程进行释放锁操作。

Condition应用

通过

     ReentrantLock lock = new ReentrantLock();     Condition condition = lock.newCondition(); 

获得Condition对象(Condition是J.U.C下locks包下的接口)。

通过Condition对象的.await(*),可以将当前线程的线程状态切换到Waiting状态(如果是有参,则是Time Waiting状态)。而.signal(),.signalAll()等方法则正好相反,恢复线程状态为Runnable状态。

ReentrantReadWriteLock

简介

ReentrantLock和Synchronized功能类似,更加灵活,当然,也更加手动了。

大家都知道,只有涉及资源的竞争时,采用同步的必要。写操作自然属于资源的竞争,但是读操作并不属于资源的竞争行为。简单说,就是写操作最多只能一个线程(因为写操作涉及数据改变,多个线程同时写,会产生资源同步问题),而读操作可以有多个(因为不涉及数据改变)。

所以在读多写少的场景下,ReentrantLock就比较浪费资源了。这就需要一种能够区分读写操作的锁,那就是ReentrantReadWriteLock。通过ReentrantReadWriteLock,可以获得读锁与写锁。当写锁存在时,有且只能有一个线程持有锁。当写锁不存在时,可以有多个线程持有读锁(写锁,必须等待读锁释放完,才可以持有锁)。

Lock及Condition应用

         ReentrantReadWriteLock lock = new ReentrantReadWriteLock();          ReentrantReadWriteLock.ReadLock readLock = lock.readLock();         readLock.lock();         readLock.unlock();          readLock.newCondition();          ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();         writeLock.lock();         writeLock.unlock();          writeLock.newCondition(); 

与之前ReentrantLock应用的区别,就是需要通过lock.readLock()与lock.writeLock()来获取读锁,写锁,再进行加锁,释放锁的操作,以及Condition的获取操作。

二,手写ReentrantLock

获取需求

终于上大餐了。

首先第一步操作,我们需要确定我们要做什么。

我们要做一个锁,这里姑且命名为JarryReentrantLock。

这个锁,需要具备以下特性:可重入锁,悲观锁。

另外,为了更加规范,以后更好地融入到AQS中,该锁需要实现Lock接口。

而Lock的方法签名,在文章一开始,就已经写了,这里不再赘述。

当然,我们这里只是一个demo,所以就不实现Condition了。另外tryLock(long,TimeUnit)也不再实现,因为实现了整体后,这个实现其实并没有想象中那么困难。

JarryReentrantLock实现原理

既然需要已经确定,并且API也确定了。

那么第二步操作,就是简单思考一下,如何实现。

类成员方面:

  1. 首先,我们需要一个owner属性,来保存持有锁的线程对象。

  2. 其次,由于是可重入锁,所以我们需要一个count来保存重入次数。

  3. 最后,我们需要一个waiters属性,来保存那些竞争锁失败后,还在等待(不死不休型)的线程对象。

类方法方面:

  • tryLock:尝试获取锁,成功返回true,失败返回false。首先是获取锁的行为,可以通过CAS操作实现,或者更简单一些,通过Atomic包实现(其底层也还是CAS)。另外,由于是可重入锁,所以在尝试获取锁时,需要判断尝试获取锁的线程是否为当前锁的持有者线程。
  • lock:尝试获取锁,直到成功获得锁。看到这种不成功便成仁的精神,我第一个想法是循环调用tryLock。但是这实在太浪费资源了(毕竟长时间的忙循环是非常消耗CPU资源的)。所以就是手动通过LockSupport.park()将当前线程挂起,然后置入等待队列waiters中,直到释放锁操作来调用。
  • tryUnlock:尝试解锁,成功返回true,失败返回false。首先就是在释放锁前,需要判断尝试解锁的线程与锁的持有者是否为同一个线程(总不能线程A把线程B持有的锁给释放了吧)。其次,需要判断可重入次数count是否为0,从而决定是否将锁的持有owner设置为null。最后,就是为了避免在count=0时,其他线程同时进行加锁操作,造成的count>0,owner=null的情况,所以count必须是Atomic,并此处必须采用CAS操作(这里有些难理解,可以看代码,有相关注释)。
  • unlock:解锁操作。这里尝试进行解锁,如果解锁成功,需要从等待队列waiters中唤醒一个线程(唤醒后的线程,由于在循环中,所以会继续进行竞争锁操作。但是切记该线程不一定竞争锁成功,因为可能有新来的线程,抢先一步。那么该线程会重新进入队列。所以,此时的JarryReentrantLock只支持不公平锁)。

JarryReentrantLock实现

那么接下来,就根据之前的信息,进行编码吧。

     package tech.jarry.learning.netease;          import java.util.concurrent.LinkedBlockingQueue;     import java.util.concurrent.TimeUnit;     import java.util.concurrent.atomic.AtomicInteger;