Java并发编程-ReentrantLock
上文已经总结了AQS的前世今生,有了这个基础我们就可以来进一步学习并发工具类。首先我们要学习的就是ReentrantLock,本文将从ReentrantLock的产生背景、源码原理解析和应用来学习ReentrantLock这个并发工具类。
1、 产生背景
前面我们已经学习过了synchronized,这个关键字可以确保对象在并发访问中的原子性、可见性和有序性,这个关键字的底层交由了JVM通过C++来实现,既然是JVM实现,就依赖于JVM,程序员就无法在Java层面进行扩展和优化,肯定就灵活性不高,比如程序员在使用时就无法中断一个正在等待获取锁的线程,或者无法在请求一个锁时无限的等待下去。基于这样一个背景,Doug Lea构建了一个在内存语义上和synchronized一样效果的Java类,同时还扩展了其他一些高级特性,比如定时的锁等待、可中断的锁等待和公平性等,这个类就是ReentrantLock。
2、 源码原理解析
2.1 可重入性原理
在synchronized一文中,我们认为synchronized是一种重量级锁,它的实现对应的是C++的ObjectMonitor,代码如下:
ObjectMonitor() { _header = NULL; _count = 0; //记录线程获取锁的次数 _waiters = 0; _recursions = 0; //锁的重入次数 _object = NULL; _owner = NULL;//指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //等待条件队列 类似AQS的ConditionObject _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //同步队列 类似AQS的CLH队列 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
从代码中可以看到synchronized实现的锁的重入依赖于JVM,JVM为每个对象的锁关联一个计数器_count和一个所有者线程_owner,当计数器为0的时候就认为锁没有被任何线程持有,当线程请求一个未被持有的锁时,JVM就记下锁的持有者,并将计数器的值设置为1,如果是同一个线程再次获取这个锁,计数器的值递增,而当线程退出时,计数器的值递减,直到计数器为0时,锁被释放。
ReentrantLock实现了在内存语义上的synchronized,固然也是支持可重入的,那么ReentrantLock是如何支持的呢,让我们以非公平锁的实现看下ReentrantLock的可重入,代码如下:
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; } protected final boolean tryRelease(int releases) { int c = getState() - releases; //既然可重入 就需要释放重入获取的锁 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true;//只有线程全部释放才返回true setExclusiveOwnerThread(null); //同步队列的线程都可以去获取同步状态了 } setState(c); return free; }
看到这也就明白了上文说的ReentrantLock类使用AQS同步状态来保存锁重复持有的次数。当锁被一个线程获取时,ReentrantLock也会记录下当前获得锁的线程标识,以便检查是否是重复获取,以及当错误的线程试图进行解锁操作时检测是否存在非法状态异常。
2.2 获取和释放锁
如下是获取和释放锁的方法:
public void lock() { sync.lock();//获取锁 } public void unlock() { sync.release(1); //释放锁 }
获取锁的时候依赖的是内部类Sync的lock()方法,该方法又有2个实现类方法,分别是非公平锁NonfairSync和公平锁FairSync,具体咱们下一小节分析。再来看下释放锁,释放锁的时候实际调用的是AQS的release方法,代码如下:
public final boolean release(int arg) { if (tryRelease(arg)) {//调用子类的tryRelease 实际就是Sync的tryRelease Node h = head;//取同步队列的头节点 if (h != null && h.waitStatus != 0)//同步队列头节点不为空且不是初始状态 unparkSuccessor(h);//释放头节点 唤醒后续节点 return true; } return false; }
Sync的tryRelease就是上一小节的重入释放方法,如果是同一个线程,那么锁的重入次数就依次递减,直到重入次数为0,此方法才会返回true,此时断开头节点唤醒后续节点去获取AQS的同步状态。
2.3 公平锁和非公平锁
公平锁还是非公平锁取决于ReentrantLock的构造方法,默认无参构造方法是NonfairSync,含参构造方法,入参true为FairSync,入参false为NonfairSync。
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
再分别来看看非公平锁和公平锁的实现。
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1))//通过CAS来获取同步状态 也就是锁 setExclusiveOwnerThread(Thread.currentThread());//获取成功线程占有锁 else acquire(1);//获取失败 进入AQS同步队列排队等待 执行AQS的acquire方法 } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
在AQS的acquire方法中先调用子类tryAcquire,也就是nonfairTryAcquire,见2.1小节。可以看出非公平锁中,抢到AQS的同步状态的未必是同步队列的首节点,只要线程通过CAS抢到了同步状态或者在acquire中抢到同步状态,就优先占有锁,而相对同步队列这个严格的FIFO队列来说,所以会被认为是非公平锁。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1);//严格按照AQS的同步队列要求去获取同步状态 } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState(); if (c == 0) {//锁未被抢占 if (!hasQueuedPredecessors() &&//没有前驱节点 compareAndSetState(0, acquires)) {//CAS获取同步状态 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) {//锁已被抢占且线程重入 int nextc = c + acquires;//同步状态为重入次数 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
公平锁的实现直接调用AQS的acquire方法,acquire中调用tryAcquire。和非公平锁相比,这里不会执行一次CAS,接下来在tryAcquire去抢占锁的时候,也会先调用hasQueuedPredecessors看看前面是否有节点已经在等待获取锁了,如果存在则同步队列的前驱节点优先。
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. 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());//头节点的后续节点为空或者不是当前线程 }
虽然公平锁看起来在公平性上比非公平锁好,但是公平锁为此付出了大量线程切换的代价,而非公平锁在锁的获取上不能保证公平,就有可能出现锁饥饿,即有的线程多次获取锁而有的线程获取不到锁,没有大量的线程切换保证了非公平锁的吞吐量。
3、 应用
3.1普通的线程锁
标准形式如下:
ReentrantLock lock = new ReentrantLock(); try { lock.lock(); //…… }finally { lock.unlock(); }
这种用法和synchronized效果是一样的,但是必须显示的声明lock和unlock。
3.2 带限制的锁
public boolean tryLock()// 尝试获取锁,立即返回获取结果 轮询锁 public boolean tryLock(long timeout, TimeUnit unit)//尝试获取锁,最多等待 timeout 时长 超时锁 public void lockInterruptibly()//可中断锁,调用线程 interrupt 方法,则锁方法抛出 InterruptedException 中断锁
具体可查看github链接里面的ReentrantLockTest。
3.3 等待/通知模型
内置队列存在一些缺陷,每个内置锁只能关联一个条件队列(_WaitSet),这导致多个线程可能会在同一个条件队列上等待不同的条件谓词,如果每次使用notify唤醒条件队列,可能会唤醒错误的线程导致唤醒失败,但是如果使用notifyAll的话,能唤醒到正确的线程,因为所有的线程都会被唤醒,这也带来一个问题,就是不应该被唤醒的在被唤醒后发现不是自己等待的条件谓词转而又被挂起。这样的操作会带来系统的资源浪费,降低系统性能。这个时候推荐使用显式的Lock和Condition来替代内置锁和条件队列,从而控制多个条件谓词的情况,达到精确的控制线程的唤醒和挂起。具体后面再来分析下JVM的内置锁、条件队列模型和显式的Lock、Condition模型,实际上在AQS里面也提到了Lock、Condition模型。
3.4 和synchronized比较
两者的区别大致如下:
synchronized |
ReentrantLock |
使用Object本身的wait、notify、notifyAll调度机制 |
与Condition结合进行线程的调度 |
显式的使用在同步方法或者同步代码块 |
显式的声明指定起始和结束位置 |
托管给JVM执行,不会因为异常、或者未释放而发生死锁 |
手动释放锁 |
Jdk1.6之前,ReentrantLock性能优于synchronized,不过1.6之后,synchronized做了大量的性能调优,而且synchronized相对程序员来说,简洁熟悉,如果不是synchronized无法实现的功能,如轮询锁、超时锁和中断锁等,推荐首先使用synchronized,而针对锁的高级功能,再使用ReentrantLock。
参考资料:
https://github.com/lingjiango/ConcurrentProgramPractice
https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html