ReentrantLock是可重入的独占锁,同时只能有一个线程能够获取该锁,其余获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面。java
首先咱们先看一下ReentrantLock的类图结构,以下图所示:数组
从类图能够知道,ReentrantLock最终仍是使用AQS来实现,而且根据参数决定内部是公平锁仍是非公平锁,默认是非公平锁。安全
首先咱们先看ReentrantLock源码,看到其构造函数及其参数,这是决定内部是公平锁仍是非公平锁,以下源码所示:函数
public ReentrantLock() { sync = new NonfairSync(); }
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
其中类Sync直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的公平和非公平策略。ui
在这里AQS的状态值state表明线程获取该锁的可重入次数,默认状况下state的值为0,标示当前锁没有被任何线程持有,当一个线程第一次获取该锁的时候会尝试使用CAS设置state的值为1,若是CAS成功则当前线程获取了该锁,而后记录该锁的持有者为当前线程,spa
在该线程没有释放锁,第二次获取该锁后,状态值会加1,被设置为2,这就是可重入次数,在该线程释放该锁的时候,会尝试使用CAS让状态值减1,若是减1 后状态值为0 则当前线程释放该锁。线程
接下来咱们看一下ReentrantLock是如何获取锁的,以下:code
1.void lock() 当一个线程调用该方法,说明该线程但愿获取该锁,若是锁当前没有被其它线程占用而且当前线程以前没有获取该锁,则当前线程会获取到该锁,而后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 后直接返回。blog
若是当前线程以前已经获取过该锁,则此次只是简单的把 AQS 的状态值 status 加 1 后返回。 若是该锁已经被其它线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。源码以下:继承
public void lock() { sync.lock(); }
如上面代码所示,ReentrantLock的lock()是委托给sync类,根据建立ReentrantLock的时候,构造函数选择sync的实现是NonfairSync或者FairSync,这里先看sync的子类NonfairSync的状况,也就是非公平锁的时候,源码以下:
final void lock() { //(1)CAS设置状态值 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else //(2)调用AQS的acquire方法 acquire(1); }
如上面代码所示,代码(1)由于默认AQS的状态值为0,因此第一个调用Lock的线程会经过CAS设置状态值为1,CAS成功则表明当前线程获取到了锁,而后setExclusiveOwnerThread 设置了该锁持有者是当前线程。
若是这时候有其余线程调用lock方法企图获取该锁,执行代码(1)CAS会失败,而后会调用AQS的acquire方法,这里注意传递参数为1,接下来咱们看AQS的acquire的核心代码,以下:
public final void acquire(int arg) { //(3)调用ReentrantLock重写的tryAcquire方法 if (!tryAcquire(arg) && // tryAcquiref返回false会把当前线程放入AQS阻塞队列 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
以前说过 AQS 并无提供可用的 tryAcquire 方法,tryAcquire 方法须要子类本身定制化,因此这里代码(3)会调用 ReentrantLock 重写的 tryAcquire 方法代码。
这里先看下非公平锁的源码代码以下:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //(4)当前AQS状态值为0 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }//(5)当前线程是该锁持有者 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; }//(6) return false; }
正如上面代码(4)会看当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁(尝试将 AQS 的状态值从 0 设置为 1),并设置当前锁的持有者为当前线程返回返回 true。
若是当前状态值不为0 则说明该锁已经被某个县城持有,因此代码(5)看当前线程是不是该锁的持有者,若是当前线程是该锁持有者,状态值增长1,而后返回true。
若是当前线程不是锁的持有者则返回 false, 而后会被放入 AQS 阻塞队列。
到目前为止,介绍完了非公平锁的实现代码,回过头看看非公平锁在这里是怎么体现的,首先非公平是说:先尝试获取锁的线程并不必定比后尝试获取锁的线程优先获取锁。
这里假设线程 A 调用 lock()方法时候执行到了 nonfairTryAcquire 的代码(4)发现当前状态值不为 0,因此执行代码(5)发现当前线程不是线程持有者,则执行代码(6)返回 false,而后当前线程会被放入了 AQS 阻塞队列。
这时候线程 B 也调用了 lock() 方法执行到 nonfairTryAcquire 的代码(4)时候发现当前状态值为 0 了(假设占有该锁的其它线程释放了该锁)因此经过 CAS 设置获取到了该锁。而明明是线程 A 先请求获取的该锁那,这就是非公平锁的实现,
这里线程 B 在获取锁前并无看当前 AQS 队列里面是否有比本身请求该锁更早的线程,而是使用了抢夺策略。
好了,知道非公平锁的实现了,那么咱们接下来看一下公平锁是如何实现的呢?
公平锁的实现只须要看FairSync重写的tryAcquire方法,源码以下:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //(7)当前AQS状态值为0 if (c == 0) { //(8)公平性策略 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //(9)当前线程是该锁持有者 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; }//(10) return false; } }
如上代码公平性的tryAcquire策略与非公平锁的相似,不一样在于代码(8)处设置CAS前添加了hasQueuedPredecessors 方法,该方法是实现公平性的核心代码,源代码以下:
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t &&((s = h.next) == null || s.thread != Thread.currentThread()); }
如上代码所示,若是当前线程节点有前驱节点则返回true,不然若是当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。
其中若是h == t 则说明当前队列为空则直接返回false,若是h != t 而且 (s = h.next) ==null 说明有一个元素将要做为AQS的第一个节点入队列,那么返回true, 若是h != t 而且 (s = h.next) !=null 而且 s.thread != Thread.currentThread() 则说明队列里面的第一个元素不是当前线程则返回 true。
2.void lockInterruptibly() 与 lock() 方法相似,不一样在于该方法对中断响应,就是当前线程在调用该方式时候,若是其它线程调用了当前线程线程的 interrupt()方法,当前线程会抛出 InterruptedException 异常而后返回,源代码以下:
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public final void acquireInterruptibly(int arg)throws InterruptedException { //当前线程被中断则直接抛出异常 if (Thread.interrupted()) throw new InterruptedException(); //尝试获取资源 if (!tryAcquire(arg)) //调用AQS可被状态的方法 doAcquireInterruptibly(arg); }
3.boolean tryLock() 尝试获取锁,若是当前该锁没有被其它线程持有则当前线程获取该锁并返回 true, 否者返回 false,注意该方法不会引发当前线程阻塞。源码以下所示:
public boolean tryLock() { return sync.nonfairTryAcquire(1); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
如上代码与非公平锁的 tryAcquire() 方法相似,因此 tryLock() 使用的是非公平策略。
4.boolean tryLock(long timeout, TimeUnit unit) 尝试获取锁与 tryLock()不一样在于设置了超时时间,若是超时没有获取该锁则返回 false。源代码以下:
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException { //调用AQS的tryAcquireNanos方法。 return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
接下来咱们要看一下,ReentrantLock是如何释放锁的。
1.void unlock() 尝试释放锁,若是当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS 状态值减一,若是减去 1 后当前状态值为 0 则当前线程会释放对该锁的持有,否者仅仅减一而已。
若是当前线程没有持有该锁调用了该方法则会抛出 IllegalMonitorStateException 异常 ,源代码以下:
public void unlock() { sync.release(1); } protected final boolean tryRelease(int releases) { //(11)若是不是锁持有者调用UNlock则抛出异常。 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //(12)若是当前可重入次数为0,则清空锁持有线程 if (c == 0) { free = true; setExclusiveOwnerThread(null); } //(13)设置可重入次数为原始值-1 setState(c); return free; }
如上代码所示(11)若是当前线程不是该锁持有者则直接抛异常,不然,看状态值剩余值是否为0,为0 则说明当前线程要释放对该锁的持有权,则执行代码(12)把当前锁持有者设置为null。
若是剩余值不为0,则仅仅让当前线程对该锁的可重入次数减1。
到目前基本了解了ReentrantLock的原理,那么接下来咱们是否能够用ReentrantLock来实现一个简单的线程安全的list呢?
例子以下:
import java.util.ArrayList; import java.util.concurrent.locks.ReentrantLock; /** * Created by cong on 2018/6/12. */ public class ReentrantLockList { //线程不安全的list private ArrayList<String> array = new ArrayList<String>(); //独占锁 private volatile ReentrantLock lock = new ReentrantLock(); //添加元素 public void add(String e) { lock.lock(); try { array.add(e); } finally { lock.unlock(); } } //删元素 public void remove(String e) { lock.lock(); try { array.remove(e); } finally { lock.unlock(); } } //获取数据 public String get(int index) { lock.lock(); try { return array.get(index); } finally { lock.unlock(); } } }
如上代码经过在操做 array 元素前进行加锁保证同时只有一个线程能够对 array 数组进行修改,可是同时也只能有一个线程对 array 元素进行访问。
最后几个图加深前面所学的内容,以下图所示:
如上图,假如线程 Thread1,Thread2,Thread3 同时尝试获取独占锁 ReentrantLock,假设 Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点后放入 ReentrantLock 对应的 AQS 阻塞队列后阻塞挂起。
如上图,假设 Thread1 获取锁后调用了对应的锁建立的条件变量 1,那么 Thread1 就会释放获取到的锁,而后当前线程就会被转换为 Node 节点后插入到条件变量 1 的条件队列,因为 Thread1 释放了锁,
因此阻塞到 AQS 队列里面 Thread2 和 Thread3 就有机会获取到该锁,假如使用的公平策略,那么这时候 Thread2 会获取到该锁,会从 AQS 队列里面移除 Thread2 对应的 Node 节点。