AQS与ReentrantLock详解
本章内容是AQS原理,但仅谈AQS会显得很乱,因此以它的其中一个实现ReentrantLock进行切入。
一、ReentrantLock功能详解
ReentrantLock是一个可重入的互斥锁,它具有与synchronized所代表的隐式监视器锁相同的一些基本行为和语义,但它的功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
1.1 ReentrantLock是可重入且互斥的
1 public class Test3 { 2 static class RunnableTest implements Runnable{ 3 private ReentrantLock lock = new ReentrantLock(); 4 5 @Override 6 public void run() { 7 lock.lock(); 8 System.out.println(Thread.currentThread().getName()+" get the lock."); 9 try { 10 runTest(); 11 }finally { 12 lock.unlock(); 13 System.out.println(Thread.currentThread().getName()+" release the lock."); 14 } 15 } 16 17 private void runTest(){ 18 lock.lock(); 19 try { 20 TimeUnit.SECONDS.sleep(3); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 }finally { 24 System.out.println(Thread.currentThread().getName()+" has finished the task."); 25 lock.unlock(); 26 } 27 } 28 } 29 30 public static void main(String[] args) { 31 RunnableTest runnableTest = new RunnableTest(); 32 new Thread(runnableTest).start(); 33 new Thread(runnableTest).start(); 34 new Thread(runnableTest).start(); 35 } 36 } 37 //Thread-0 get the lock. 38 Thread-0 has finished the task. 39 Thread-0 release the lock.
该例每次只有一个线程获得锁,是独占的;每次线程执行过程中都会获取两次锁,是可重入的。这与synchronized功能相同,不同的是synchronized的加锁解锁过程隐式的,ReentrantLock需要手动加锁和解锁,并且解锁操作要尽量放在finally块中,要保证加锁次数和解锁次数相同,否则可能会造成其他线程无法获取到锁。大规模并发下,两者性能相差不大,并且ReentrantLock的功能比synchronized更丰富:
1.2 ReentrantLock实现公平锁
1 public class Test3 { 2 public static void main(String[] args) { 3 RunnableTest test = new RunnableTest(); 4 new Thread(test, "Thread_1").start(); 5 new Thread(test, "Thread_2").start(); 6 } 7 8 static class RunnableTest implements Runnable{ 9 private ReentrantLock lock = new ReentrantLock(true); 10 @Override 11 public void run() { 12 excute(); 13 } 14 15 private void excute(){ 16 for (int i = 0;i < 2;i++){ 17 lock.lock(); 18 System.out.println(Thread.currentThread().getName()+" get the lock."); 19 try { 20 TimeUnit.SECONDS.sleep(3); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 }finally { 24 System.out.println(Thread.currentThread().getName()+" release the lock."); 25 lock.unlock(); 26 } 27 } 28 } 29 } 30 } 31 公平执行结果: 32 Thread_1 get the lock. 33 Thread_1 release the lock. 34 Thread_2 get the lock. 35 Thread_2 release the lock. 36 Thread_1 get the lock. 37 Thread_1 release the lock. 38 Thread_2 get the lock. 39 Thread_2 release the lock. 40 非公平执行结果: 41 Thread_1 get the lock. 42 Thread_1 release the lock. 43 Thread_1 get the lock. 44 Thread_1 release the lock. 45 Thread_2 get the lock. 46 Thread_2 release the lock. 47 Thread_2 get the lock. 48 Thread_2 release the lock.
每个线程执行任务时会获取两次锁,第一次释放锁后立刻再去获取锁。公平锁下,这些锁倾向于将访问权授予等待时间最长的线程,与非公平锁相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量,但是在获得锁和保证锁分配的均衡性时差异较小。不过,公平锁不能保证线程调度的公平性。另外,未定时的 tryLock 方法并没有使用公平设置,只要该锁未被别的线程持有就立即获取锁。
1.3 ReentrantLock可响应中断与限时等待
synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,阻塞在锁上的线程除非获得锁否则将一直等待下去。ReentrantLock提供了可响应中断的方法lockInterruptibly( )。
1 public class Test4{ 2 public static void main(String[] args) throws InterruptedException { 3 ReentrantLock lock1 = new ReentrantLock(); 4 ReentrantLock lock2 = new ReentrantLock(); 5 Thread thread1 = new Thread(new RunnableTest(lock1, lock2), "Thread_1"); 6 Thread thread2 = new Thread(new RunnableTest(lock2, lock1), "Thread_2"); 7 thread1.start(); 8 thread2.start(); 9 10 TimeUnit.SECONDS.sleep(2); 11 12 thread1.interrupt(); 13 } 14 15 static class RunnableTest implements Runnable{ 16 private ReentrantLock lock1; 17 private ReentrantLock lock2; 18 19 public RunnableTest(ReentrantLock lock1,ReentrantLock lock2) { 20 this.lock1 = lock1; 21 this.lock2 = lock2; 22 } 23 24 @Override 25 public void run() { 26 try { 27 lock1.lockInterruptibly(); 28 TimeUnit.SECONDS.sleep(1); 29 System.out.println(Thread.currentThread().getName()+" get another lock"); 30 lock2.lockInterruptibly(); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 }finally { 34 lock1.unlock(); 35 lock2.unlock(); 36 } 37 } 38 } 39 } 40 //Thread_2 get another lock 41 Thread_1 get another lock 42 java.lang.InterruptedException
该方法,如果锁被另一个线程保持,那么在其他某个线程中断当前线程之前或者当前线程获取到锁之前,该线程将一直处于休眠状态。该死锁案例中,线程1在获取lock2时陷入休眠,直到被中断,获取锁失败。
除此之外,ReentrantLock还提供获取锁时的限时等待方法tryLock(long timeout, TimeUnit unit)。超时后返回false,该方法遵守公平设置,而tryLock( )方法不遵守公平设置。
1 public class Test5{ 2 public static void main(String[] args) throws InterruptedException { 3 RunnableTest test = new RunnableTest(); 4 new Thread(test, "Thread_1").start(); 5 new Thread(test, "Thread_2").start(); 6 } 7 8 static class RunnableTest implements Runnable{ 9 private ReentrantLock lock = new ReentrantLock(); 10 11 @Override 12 public void run() { 13 try { 14 if (lock.tryLock(3, TimeUnit.SECONDS)) { 15 System.out.println(Thread.currentThread().getName() + " get the lock"); 16 TimeUnit.SECONDS.sleep(5); 17 } 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 }finally { 21 lock.unlock(); 22 } 23 } 24 } 25 } 26 //hread_1 get the lock 27 Exception in thread "Thread_2"
1.4 ReentrantLock中的通信
synchronized通过wait( )和notify( )实现线程通信,ReentrantLock替代了 synchronized 方法和语句的使用,也通过Condition替代了Object 监视器方法的使用。Condition条件使用方法与wait( )方法相同,使用前需要先获取到锁,然后await( )方法将锁释放掉,该线程在Condition条件下等待,知道其他线程通过signalAll( )唤醒该线程。
1 public class ProduceAndConsume { 2 private ReentrantLock lock = new ReentrantLock(); 3 private Condition condition = lock.newCondition(); 4 private int i; 5 private final static int MAXVALUE = 2; 6 7 public void produce() { 8 lock.lock(); 9 try { 10 while (MAXVALUE <= i) 11 condition.await(); 12 System.out.println(Thread.currentThread().getName()+" 生产 "+(++i)); 13 condition.signalAll(); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 }finally { 17 lock.unlock(); 18 } 19 } 20 21 public void consume(){ 22 lock.lock(); 23 try { 24 while (MAXVALUE > i) 25 condition.await(); 26 System.out.println(Thread.currentThread().getName()+" 消费 "+(--i)); 27 condition.signalAll(); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } finally { 31 lock.unlock(); 32 } 33 } 34 35 public static void main(String[] args) { 36 ProduceAndConsume produceAndConsume = new ProduceAndConsume(); 37 new Thread(new Runnable() { 38 @Override 39 public void run() { 40 while (true) { 41 produceAndConsume.produce(); 42 } 43 } 44 }, "Thread1").start(); 45 46 new Thread(new Runnable() { 47 @Override 48 public void run() { 49 while (true) { 50 produceAndConsume.consume(); 51 } 52 } 53 }, "Thread2").start(); 54 } 55 }
1 public class ProduceAndConsume2 { 2 final Lock lock = new ReentrantLock(); 3 final Condition notFull = lock.newCondition(); 4 final Condition notEmpty = lock.newCondition(); 5 6 final Object[] items = new Object[10]; 7 int putptr, takeptr, count; 8 9 public void put(Object x) throws InterruptedException { 10 lock.lock(); 11 try { 12 while (count == items.length) 13 notFull.await(); 14 items[putptr] = x; 15 if (++putptr == items.length) 16 putptr = 0; 17 ++count; 18 notEmpty.signal(); 19 }finally { 20 lock.unlock(); 21 } 22 } 23 24 public Object take() throws InterruptedException { 25 lock.lock(); 26 try { 27 while (count == 0) 28 notEmpty.await(); 29 Object x = items[takeptr]; 30 if (++takeptr == items.length) 31 takeptr = 0; 32 --count; 33 notFull.signal(); 34 return x; 35 } finally { 36 lock.unlock(); 37 } 38 } 39 }
二、AQS简介
AQS(AbstractQueuedSynchronizer)以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。
总的来说AQS为实现同步器提供了一个框架,具体实现由子类自己扩展,这个同步器依赖于一个先进先出的队列、阻塞的锁和表示状态的单个原子int值。接下来根据源码具体讨论。
2.1 Node类
Node是阻塞队列的节点类,注释中作者用很多文字讲解CLH,因为该AQS是CLH的一个变种,但没必要去纠结CLH。
1 static final class Node { 2 //共享模式下的等待节点 3 static final Node SHARED = new Node(); 4 //独占模式下的等待节点 5 static final Node EXCLUSIVE = null; 6 7 //当前等待节点的线程已被取消 8 static final int CANCELLED = 1; 9 //当前节点需要唤醒其后续节点的线程 10 static final int SIGNAL = -1; 11 //当前等待节点的线程在Condition上等待 12 static final int CONDITION = -2; 13 //共享模式下,表示下次同步状态会无条件传播下去 14 static final int PROPAGATE = -3; 15 16 //共5个值 17 volatile int waitStatus; 18 19 //前置节点 20 volatile Node prev; 21 22 //后继节点 23 volatile Node next; 24 25 //阻塞的线程,用完后置为null回收 26 volatile Thread thread; 27 28 /** 29 *用于条件队列 30 */ 31 Node nextWaiter; 32 33 //用于创建head或为nextWaiter设置共享标记 34 Node() { 35 } 36 37 //addWaiter使用 38 Node(Thread thread, Node mode) { 39 this.nextWaiter = mode; 40 this.thread = thread; 41 } 42 43 //Condition使用 44 Node(Thread thread, int waitStatus) { 45 this.waitStatus = waitStatus; 46 this.thread = thread; 47 } 48 }
Node类中包含几个int值,通过让赋予waitStatus不同的值,来表示当前节点状态(也反映出线程状态):
- SIGNAL:此节点的后继节点(或将很快)被阻塞(通过park),因此当前节点在释放锁的时候或者被取消时收必须unpark它的后继节点。为了避免竞争,获取方法必须一开始就表明它们需要signal,然后重新原子获取锁,如果失败则阻塞。
- CANCELLED:当前节点由于被中断或者超时而被取消。那么这个节点的status一直保持着取消的状态,并且再也不能参与锁的竞争了,直到某时刻被回收。
- CONDITION:这个节点现在在条件队列中。这个状态将不会在同步队列中的节点,直到这个节点的状态由condition变成0,那么将会把这个节点移到同步队列中。
- PROPAGATE:在共享模式下释放锁的时候,应该把同步状态传播给其他节点。即使已经做了其他操作,但是为了保证传播的继续,在共享模式下释放同步状态的时候,应该给节点(只有头结点)的状态设置为progate。
- 0:不是上面四种状况的情景
上面值的排列可以简化使用,只要值为负,意味着不需要唤醒,大多数时候仅仅只用判断值的符号即可1。对于普通同步节点初始化值为0,对于条件节点初始化为CONDITION。修改是使用CAS进行
1 public abstract class AbstractQueuedSynchronizer 2 extends AbstractOwnableSynchronizer 3 implements java.io.Serializable { 4 5 private static final long serialVersionUID = 7373984972572414691L; 6 7 protected AbstractQueuedSynchronizer() { } 8 9 /** 10 *队列的头,第一次使用时才加载。除了初始化,只有通过setHead()方法修改。只要head 11 *存在,那么其waitStatus保证不会被取消 12 */ 13 private transient volatile Node head; 14 15 /** 16 *队尾,第一次使用时才加载。仅在enq()方法中使用,以添加新的等待节点。 17 */ 18 private transient volatile Node tail; 19 20 /** 21 * 同步状态,其在不同的子类实现中的意义不同,在ReentrantLock中表示锁占有情况 22 */ 23 private volatile int state; 24 25 /** 26 *为提高响应速度,设置的自旋超时阈值(在park之前先进行此时间的自旋) 27 */ 28 static final long spinForTimeoutThreshold = 1000L; 29 30 /** 31 * 独占模式下持有同步状态(锁)的线程,这是AQS父类中的变量 32 */ 33 private transient Thread exclusiveOwnerThread; 34 }
AQS用链表维护的一个先进先出队列。这里也就是文档中说到的队列、int值和状态。目前为止这个队列还只能将线程封装进一个复杂的节点保存下来,它是如何阻塞的?用什么实现阻塞的?int值又是如何控制节点状态的?我们以ReentrantLock实现为例,跟踪其非公平模式,后续再比较公平模式。
2.2 lock方法
2.2.1 acquire方法
1 public class ReentrantLock implements Lock, java.io.Serializable { 2 3 private final Sync sync; 4 5 public ReentrantLock() { 6 sync = new NonfairSync(); 7 } 8 9 public ReentrantLock(boolean fair) { 10 sync = fair ? new FairSync() : new NonfairSync(); 11 } 12 }
ReentrantLock中默认采用非公平锁。
1 final void lock() { 2 //尝试获取设置state,设置成功则将当前线程设置为持有锁线程 3 if (compareAndSetState(0, 1)) 4 setExclusiveOwnerThread(Thread.currentThread()); 5 else 6 acquire(1); 7 }
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
lock( )方法首先尝试设置state,state在ReentrantLock表示锁被重入的次数,因此只有在锁未被任何线程(即使是当前已经持有的锁的线程)持有时才能设置成功,否则执行acquire( )方法。
acquire( )方法未被子类重写,其中有四个重要方法,tryAcquire( )方法获取锁失败返回false,才会执行后面的操作。
2.2.2 tryAcquire方法
1 protected final boolean tryAcquire(int acquires) { 2 return nonfairTryAcquire(acquires); 3 } 4 5 final boolean nonfairTryAcquire(int acquires) { 6 7 //获取当前执行此方法的线程 8 final Thread current = Thread.currentThread(); 9 int c = getState(); 10 11 //锁未被任何线程持有,尝试获取 12 if (c == 0) { 13 if (compareAndSetState(0, acquires)) { 14 setExclusiveOwnerThread(current); 15 return true; 16 } 17 } 18 //如果当前线程正持有锁,说明是重入操作,state+1获取成功 19 else if (current == getExclusiveOwnerThread()) { 20 int nextc = c + acquires; 21 if (nextc < 0) // overflow 22 throw new Error("Maximum lock count exceeded"); 23 24 //更新state值,因为当前线程已经持有锁,所以不用CAS方式进行更新 25 setState(nextc); 26 return true; 27 } 28 29 //获取锁失败 30 return false; 31 }
tryAcquirei( )是AQS定义的一个钩子方法,在公平模式和非公平模式下不同,非公平模式下在NonfairSync类中被重写,调用的是Sync中的nonfairTryAcquire( )方法。
nonfairTryAcquire( )包含了线程获取锁时的所有情况:
- state==0,锁未被任何线程获取
- state>0&¤t==ExclusiveOwnerThread,当前线程已经持有锁,再次获取锁
- state>0&¤t !=ExclusiveOwnerThread,锁已被其他线程持有
2.2.3 addWaiter方法
1 private Node addWaiter(Node mode) { 2 Node node = new Node(Thread.currentThread(), mode); 3 // Try the fast path of enq; backup to full enq on failure 4 Node pred = tail; 5 6 //pred==null说明tail和head还未初始化 7 if (pred != null) { 8 9 //将node的前置节点指向pred 10 node.prev = pred; 11 12 //pred在此刻仍然是tail,即未被其他线程更改,则将node更新为新的tail节点 13 //这是一次尝试更新 14 if (compareAndSetTail(pred, node)) { 15 pred.next = node; 16 return node; 17 } 18 } 19 enq(node); 20 return node; 21 }
1 private Node enq(final Node node) { 2 for (;;) { 3 //重新获取tail节点 4 Node t = tail; 5 6 //尾节点为null,需要对head和tail初始化 7 if (t == null) { // Must initialize 8 9 //因为同时有多个线程进行操作,所以用CAS方式进行 10 //这里head=new Node(),即waitStatus=0 11 if (compareAndSetHead(new Node())) 12 tail = head; 13 } else { 14 node.prev = t; 15 if (compareAndSetTail(t, node)) { 16 t.next = node; 17 return t; 18 } 19 } 20 } 21 }
addWaiter( )方法进行了入队操作,其中封装线程时使用Node(Thread thread, Node mode)构造方法,传入了Node的mode,ReentrantLock中mode传入是EXCLUSIVE模式。(入队时的操作,都是先将当前节点的前置节点指向尾结点,再进行判断,个人觉得这个顺序安排的很好)。另一个有趣的是addWaiter( )和enq( )中出现了两次更新tail节点的代码,addWaiter( )中的实际冗余了,不过这样的冗余却一定程度提高了效率。
2.2.4 acquireQueued方法
1 final boolean acquireQueued(final Node node, int arg) { 2 boolean failed = true; 3 try { 4 boolean interrupted = false; 5 for (;;) { 6 final Node p = node.predecessor(); 7 8 //如果node前置节点是head,则node中线程尝试获取锁 9 if (p == head && tryAcquire(arg)) { 10 11 //获取锁后首先设置node为head节点 12 //回收p,即原来的head节点 13 setHead(node); 14 p.next = null; // help GC 15 failed = false; 16 return interrupted; 17 } 18 if (shouldParkAfterFailedAcquire(p, node) && 19 parkAndCheckInterrupt()) 20 interrupted = true; 21 } 22 } finally { 23 if (failed) 24 cancelAcquire(node); 25 } 26 }
acquireQueued( )方法主要有两个部分,一是对刚入队的节点,再次尝试获取锁;二是,若该节点暂时获取不到锁,则进行阻塞操作。
1 private void setHead(Node node) { 2 head = node; 3 node.thread = null; 4 node.prev = null; 5 }
这里的setHead( )方法可以看到head节点中的内容,head节点可以理解为正持有锁的节点,但实际里面的thread已经被置空。
1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 2 int ws = pred.waitStatus; 3 if (ws == Node.SIGNAL) 4 return true; 5 6 //如果node的前置节点已经取消,则回收该取消节点 7 if (ws > 0) { 8 do { 9 node.prev = pred = pred.prev; 10 } while (pred.waitStatus > 0); 11 pred.next = node; 12 } else { 13 //将其前置节点的状态改为SIGNAL 14 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 15 } 16 return false; 17 }
shouldParkAfterFailedAcquire( )方法是获取锁失败后应该进行阻塞(一个好名字多么重要),为进行阻塞设置了相关条件。该条件是要进行阻塞的节点的前置节点,其状态必须为SINGAL(只有前置节点是SINGAL状态,当前节点在前置节点释放锁后才会被唤醒)。如果当前节点已经取消,则将当前节点一直向前,直到链接到一个未被取消的节点,取消的节点被回收;若节点未被取消,也不是SINGAL状态,则更新其状态为SINGAL状态。
我的质疑是,在前置节点是CANCELLED状态时,其向前链接的过程不是CAS进行的,安全吗?不过我也确实没想到具体的情景。
1 private final boolean parkAndCheckInterrupt() { 2 LockSupport.park(this); 3 //若是中断,返回true;若是unpark,返回false 4 return Thread.interrupted(); 5 }
shouldParkAfterFailedAcquire( )方法准备好返回true后,则用parkAndInterrupt( )方法进行阻塞。其底层是调用LockSupport的park( )方法实现的。返回时,会有判断,若当前阻塞状态是其他线程通过中断完成的,则Thread.interrupted( )会返回true,并重置此状态,则当前线程在抢到锁返回后,会在acquire( )方法中执行interrupt( )方法,当前线程被中断(这里有两次中断,一次将线程从park状态唤醒,然后重置中断标志,抢到锁后,再调用一次中断,用来中断线程进行的任务);若是通过unpark( )结束阻塞,说明当前线程是被前置节点唤醒的,则Thread.interrupted( )会返回false,线程抢到锁后继续执行任务。
1 public class Test6 { 2 private final boolean parkAndCheckInterrupt() { 3 LockSupport.park(this); 4 return Thread.interrupted(); 5 } 6 7 8 public static void main(String[] args) throws InterruptedException { 9 Test6 SPCK = new Test6(); 10 Thread thread = new Thread(new Runnable() { 11 @Override 12 public void run() { 13 System.out.println("Before Park!"); 14 if (SPCK.parkAndCheckInterrupt()) { 15 System.out.println("中断返回!"); 16 }else { 17 System.out.println("Unpark返回!"); 18 } 19 } 20 }); 21 22 thread.start(); 23 TimeUnit.SECONDS.sleep(1); 24 // thread.interrupt(); 25 LockSupport.unpark(thread); 26 } 27 }
2.2.5 cancelAcquire方法
1 private void cancelAcquire(Node node) { 2 // Ignore if node doesn't exist 3 if (node == null) 4 return; 5 6 //将其不再和线程关联 7 node.thread = null; 8 9 //将其链接到一个未被取消的前置节点后面 10 //此时node.prve==pred,但pred.next不一定是node 11 Node pred = node.prev; 12 while (pred.waitStatus > 0) 13 node.prev = pred = pred.prev; 14 15 //保存pred.next,因为经过whille后,pred.next链接的可能是其他被取消的节点 16 Node predNext = pred.next; 17 node.waitStatus = Node.CANCELLED; 18 19 //如果当前节点是tail,将其pred设为tail, 20 if (node == tail && compareAndSetTail(node, pred)) { 21 22 //将pred.next置位null 23 compareAndSetNext(pred, predNext, null); 24 } else { 25 int ws; 26 27 //如果当前节点不是tail节点,也不是head的后继节点, 28 //则将其前置节点置为SINGAL状态,并将前置节点的后继 29 //节点链接到当前节点的后继节点 30 if (pred != head && 31 ((ws = pred.waitStatus) == Node.SIGNAL || 32 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && 33 pred.thread != null) { 34 Node next = node.next; 35 if (next != null && next.waitStatus <= 0) 36 compareAndSetNext(pred, predNext, next); 37 } else { 38 39 //如果当前节点是head的后继节点,则唤醒后继节点 40 //注意此时pred.next还没有被取消,它是通过唤醒后继节点在 41 //tryAcquire( )方法中进行的 42 unparkSuccessor(node); 43 } 44 node.next = node; // help GC 45 } 46 }
此方法是在acquireQueued( )其中某处抛出异常时会调用的,作用是取消当前节点,主要分为三种情况:
- 当前节点是tail节点
- 当前节点不是tail节点,也不是head的后继节点
- 当前节点是head的后继节点
2.2.6 加锁流程
2.3 unlock方法
2.3.1 release方法
1 public void unlock() { 2 sync.release(1); 3 } 4 5 public final boolean release(int arg) { 6 7 //如果tryRelease中state状态为0,返回true 8 if (tryRelease(arg)) { 9 Node h = head; 10 if (h != null && h.waitStatus != 0) 11 //唤醒离头节点最近的未被取消到的节点 12 unparkSuccessor(h); 13 return true; 14 } 15 return false; 16 } 17
1 protected final boolean tryRelease(int releases) { 2 3 //计算state更新后的值 4 int c = getState() - releases; 5 if (Thread.currentThread() != getExclusiveOwnerThread()) 6 throw new IllegalMonitorStateException(); 7 boolean free = false; 8 9 //如果state待更新值为0,则包括重入的锁也被释放,可以清除锁被持有的标记 10 if (c == 0) { 11 free = true; 12 setExclusiveOwnerThread(null); 13 } 14 15 //设置state状态,一旦设置为0,刚加入的线程可以立即进行争抢 16 setState(c); 17 return free; 18 }
1 private void unparkSuccessor(Node node) { 2 int ws = node.waitStatus; 3 4 //尝试设置当前节点状态为0 5 if (ws < 0) 6 compareAndSetWaitStatus(node, ws, 0); 7 8 Node s = node.next; 9 //如果当前节点后置节点被取消,则从tail向前,找到离当前节点最近的未被取消的节点, 10 //然后唤醒该节点(此处并没有对那些取消的节点进行处理,而是唤醒后在tryAcquire()中处理的) 11 if (s == null || s.waitStatus > 0) { 12 s = null; 13 for (Node t = tail; t != null && t != node; t = t.prev) 14 if (t.waitStatus <= 0) 15 s = t; 16 } 17 if (s != null) 18 LockSupport.unpark(s.thread); 19 }
unlock( )方法调用了release( )方法,其中tryRelease( )方法是一个钩子方法,在Sync类中被重写,主要是更新state状态,在state状态为0时能够执行unparkSuccessor( )方法,唤醒线程。
unparkSuccessor( )方法之前存在两个条件h != null && h.waitStatus != 0:
- 因为对head和tail都是懒加载,所以如果head==null,可能只有一个线程工作,不存在其他线程争抢,阻塞队列也就根本没有初始化,没有工作;
- h.waitStatus != 0,判断此条件是因为在tryAcquire( )方法中,阻塞线程时,其前置节点必须是SINGAL状态。若h.waitStatus != 0,则说明后置节点还没有被阻塞,不需要唤醒,直接就能去抢锁。
2.4 对比非公平锁和公平锁及小结
公平锁与非公平锁在实现上的不同方法:
1 final void lock() { 2 acquire(1); 3 } 4 5 protected final boolean tryAcquire(int acquires) { 6 final Thread current = Thread.currentThread(); 7 int c = getState(); 8 if (c == 0) { 9 10 //hasQueuedPredecessors(),如果当前队列除head外,前面还有节点,返回true 11 if (!hasQueuedPredecessors() && 12 compareAndSetState(0, acquires)) { 13 setExclusiveOwnerThread(current); 14 return true; 15 } 16 } 17 else if (current == getExclusiveOwnerThread()) { 18 int nextc = c + acquires; 19 if (nextc < 0) 20 throw new Error("Maximum lock count exceeded"); 21 setState(nextc); 22 return true; 23 } 24 return false; 25 }
可以看到,非公平主要在两个地方上:
- 一是在lock( )方法实现上,非公平锁先进行了一次抢锁操作,而从上一个线程释放锁,到唤醒下一个节点,是需要时间的,此时新入新城更可能抢到锁;
- 二是tryAcquire( )方法上,非公平锁还是执行直接抢锁,抢不到再入队。而公平锁实现上,会先判断当前队列中是否有等待锁的节点,没有就取锁,有则入队(乖乖排队,完全不竞争一下)。
从ReentrantLock来看AQS,整个“锁”的过程,我们并没有看到线程拿到一个切实存在“锁”,只是以state的状态进行了表示,以ExclusiveOwnerThread进行了标识,实际上是当一个线程执行操作时,其他线程被阻塞在队列中(当然流程上讲,synchornized的实现与之相差不大)。
三、Condition接口实现
上面我们用到了await( )和signal( )实现了两个简单的生产者消费者模型,其都依赖于Condition接口。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待set(wait-set)。这也是这两个生产者消费者模型不同实现的原因,我们从源码出发:
3.1 条件队列和同步队列
首先我们需要区分这两个队列,在同步队列中,底层是回归到Node类的。在Sync队列中,如图3.1(图来源于网络),我们使用到节点中的thread、waitStatus、prve和next属性,nextWaiter属性仅仅被赋为null,表示独占,但在条件队列中,它具有更重要的作用:
图3.1 阻塞队列
每new一个Condition对象,就会产生一个对应的Condition队列,这个队列就是用nextWaiter进行串联的一个单向队列。因此实际上在条件队列中仅会用到thread、waitStatus和nextWaiter属性。
此时我们再来看上面的两个生产者消费者模型,两者的主要区别在于对Condition对象的使用。第一个使用了一个Condition,两个线程进入同一个条件队列:
第二个使用了两个Condition,两个线程进入不同Condition下的条件队列:
到这里就可以理解“Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象”,将其与synchornized关键字对比来看,使用synchornized时需要一个对象,对象关联着一个monitor对象,调用wait( )方法,线程进入等待池,notify( )后则进入锁池竞争锁,即监视器方法与该对象monitor配合使用;而使用Lock,可以产生多个Condition,不同Condition使用await( )和singal( )将阻塞和唤醒不同Condition队列中到的线程。将Condition类比为monitor中的等待池,就好像Condition对其进行了细分,使得await( )和singal( )能对应每个不同Condition,而不用与wait( )和notify( )仅作用于monitor中的唯一的等待池。因此使用Condition在受保证的通知排序,或者在执行通知时不需要保持一个锁场景下很受用。
同步队列与条件队列有什么联系呢?事实上对这两个队列的维护是独立的,不过两者之间和一定联系。如同使用synchornized时,等待池中的线程被唤醒后进入锁池竞争锁。和synchornized类似,Condition队列中的线程被singal( )后,会进入Sync队列中等待锁。需要注意的是,被唤醒的队列不是简单的直接链接在Sync队列后面,而是一个个进行转移。这种方式一方面更安全,另一个方面则是因为Condition队列中是使用nextWaiter链接的单向链表,而Sync队列是使用prev和next链接的双向链表。
3.2 ConditionObject
Condition在AQS中是通过ConditionObject实现的,只有两个核心属性,标识队头和队尾。
1 public class ConditionObject implements Condition, java.io.Serializable { 2 3 /** First node of condition queue. */ 4 private transient Node firstWaiter; 5 /** Last node of condition queue. */ 6 private transient Node lastWaiter; 7 8 /** Mode meaning to reinterrupt on exit from wait */ 9 private static final int REINTERRUPT = 1; 10 /** Mode meaning to throw InterruptedException on exit from wait */ 11 private static final int THROW_IE = -1; 12 13 /** 14 * Creates a new <tt>ConditionObject</tt> instance. 15 */ 16 public ConditionObject() { } 17 }
这里我们先分析一下,入队和出队的情况:线程是在调用await( )方法后进入Condition队列的,因此进入Condition队列之前节点是已经获取到锁的。在调用await( )后,当前线程释放掉锁,进入Condition队列。singal( )或中断后,节点从await( )继续执行,此时节点就会进入Sync队列再次竞争锁,所以唤醒后从await( )继续执行时线程一定是获取到锁的。
3.3 await方法
1 public final void await() throws InterruptedException { 2 //线程已被中断,没有加入队列的必要 3 if (Thread.interrupted()) 4 throw new InterruptedException(); 5 6 //将当前线程封装成Node,加入Condition队列 7 Node node = addConditionWaiter(); 8 9 //释放当前线程的锁,返回锁释放前线程的state 10 //释放锁后,锁被新的线程持有,释放锁的线程的节点就会从Sync中移除 11 int savedState = fullyRelease(node); 12 int interruptMode = 0; 13 14 //判断该节点是否在Sync队列中,在说明唤醒后已被转移至Sync队列 15 //不在说明刚await()从Sync队列移除了 16 while (!isOnSyncQueue(node)) { 17 LockSupport.park(this); 18 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 19 break; 20 } 21 22 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 23 interruptMode = REINTERRUPT; 24 if (node.nextWaiter != null) // clean up if cancelled 25 unlinkCancelledWaiters(); 26 if (interruptMode != 0) 27 reportInterruptAfterWait(interruptMode); 28 }
while以后的部分主要作用是在Condition队列中阻塞当前线程,并在唤醒后继续执行park( )后的代码。但唤醒的方式可以是singal( ),也可能是由于中断引起的,所以while之后的部分对此作了判断,具体在3.5进行详细讨论。
3.3.1 addConditionWaiter和fullyRelease方法
1 private Node addConditionWaiter() { 2 Node t = lastWaiter; 3 4 //如果尾节点被取消,则清除尾节点 5 if (t != null && t.waitStatus != Node.CONDITION) { 6 //删除队列中已经取消的节点 7 unlinkCancelledWaiters(); 8 t = lastWaiter; 9 } 10 11 //将当前线程封装成Condition状态的节点,加入队列尾部 12 Node node = new Node(Thread.currentThread(), Node.CONDITION); 13 if (t == null) 14 firstWaiter = node; 15 else 16 t.nextWaiter = node; 17 lastWaiter = node; 18 return node; 19 }
1 //从头遍历,删除状态不为Condition的节点 2 private void unlinkCancelledWaiters() { 3 Node t = firstWaiter; 4 Node trail = null; 5 while (t != null) { 6 Node next = t.nextWaiter; 7 if (t.waitStatus != Node.CONDITION) { 8 t.nextWaiter = null; 9 if (trail == null) 10 firstWaiter = next; 11 else 12 trail.nextWaiter = next; 13 14 if (next == null) 15 lastWaiter = trail; 16 } 17 else 18 trail = t; 19 t = next; 20 } 21 }
从这里我们可以看到Sync队列和Condition队列的不同:
- Sync队列有dummy节点,Condition队列没有
- Sync队列初始状态为0,Condition队列初始状态为Condition
- Sync队列有是一个双向队列,Condition队列是一个单向队列
1 final int fullyRelease(Node node) { 2 boolean failed = true; 3 try { 4 //获取节点state 5 int savedState = getState(); 6 7 //释放锁 8 if (release(savedState)) { 9 failed = false; 10 return savedState; 11 } else { 12 throw new IllegalMonitorStateException(); 13 } 14 } finally { 15 //失败,则取消该节点 16 if (failed) 17 node.waitStatus = Node.CANCELLED; 18 } 19 }
await( )方法的目的就是释放掉当前线程持有的锁,也就是靠fullyRelease( )方法完成的。该方法获取当前线程state,该属性在ReentrantLock中,表示线程持有锁的状态(包括重入),在release( )方法中传入state值,即一次释放掉所有的锁。
3.3.2 isOnSyncQueue和findNodeFromTail方法
1 final boolean isOnSyncQueue(Node node) { 2 //如果waitStatus为CONDITION,或前置节点为null,说明已不在阻塞队列中 3 if (node.waitStatus == Node.CONDITION || node.prev == null) 4 return false; 5 //如果当前节点有后继节点,肯定在队列中 6 if (node.next != null) 7 return true; 8 9 //排除上面情况,队尾节点prev不为null,后继节点为null,再从队尾向前找 10 return findNodeFromTail(node); 11 }
1 private boolean findNodeFromTail(Node node) { 2 Node t = tail; 3 for (;;) { 4 if (t == node) 5 return true; 6 if (t == null) 7 return false; 8 t = t.prev; 9 } 10 }
首先在该调用中,此方法传入的Node是addWaiter( )方法返回的Condition队列中使用的节点,因此该节点一定是CONDITION或CANCELLED状态,且其prev字段一定是null,而在Sync队列中,节点状态一定是0、SINGAL或CANCELLED状态,只有head节点的prev是null,但head状态节点一定是SINGAL状态。
3.4 singal方法
1 public final void signal() { 2 //执行singal()的线程是否持有锁,未持有就抛出异常 3 if (!isHeldExclusively()) 4 throw new IllegalMonitorStateException(); 5 Node first = firstWaiter; 6 //若条件队列不为null,执行doSingal() 7 if (first != null) 8 doSignal(first); 9 }
1 public final void signalAll() { 2 if (!isHeldExclusively()) 3 throw new IllegalMonitorStateException(); 4 Node first = firstWaiter; 5 if (first != null) 6 doSignalAll(first); 7 }
首先执行唤醒操作的必须是已经获取到锁的线程,不是就会抛出异常;然后该Condition条件队列必须存在,才能执行唤醒操作。
3.4.1 doSingal和doSingalAll方法
1 //作用节点从条件队列中断开,即断开nextWaiter 2 private void doSignal(Node first) { 3 do { 4 //断开上一个节点和下一个节点 5 //若遍历到最后一个节点,将lastWaiter节点置为null 6 if ( (firstWaiter = first.nextWaiter) == null) 7 lastWaiter = null; 8 first.nextWaiter = null; 9 10 //将断开的节点转移至阻塞队列,转移失败则继续转移下一个节点 11 //直到有一个转移成功的节点或条件队列为null 12 } while (!transferForSignal(first) && 13 (first = firstWaiter) != null); 14 }
1 private void doSignalAll(Node first) { 2 lastWaiter = firstWaiter = null; 3 do { 4 Node next = first.nextWaiter; 5 first.nextWaiter = null; 6 transferForSignal(first); 7 first = next; 8 } while (first != null); 9 }
这两个方法主要作用是将Condition队列中的Node,一个一个转移至Sync队列。doSingal( )从头遍历,直到有一个节点成功转移到Sync队列后结束,doSingalAll( )将所有节点转移至Sync队列后结束。
3.4.2 transferForSingal方法
1 final boolean transferForSignal(Node node) { 2 //在将节点插入阻塞队列之前,将节点状态设置为0 3 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) 4 return false; 5 6 //将节点插入阻塞队列,返回的p是当前节点插入之前的tail 7 Node p = enq(node); 8 int ws = p.waitStatus; 9 10 //如果p被取消或者不能将该节点状态设置为SINGAL,就唤醒当前状态 11 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 12 LockSupport.unpark(node.thread); 13 return true; 14 }
该方法将节点从Condition队列转移至Sync队列,首先在doSingal( )方法中节点已经断开nextWaiter(为null),尝试将节点状态设置为0,若失败,则说明该节点是已经被取消,返回false后被回收。
若状态设置成功,则将其插入Sync队列,这里enq( )方法返回的是之前的tail节点,然后有对该tail节点有一个判断。出现这个判断的原因在于enq( )仅仅是将节点插入队列,并没有对之前的节点进行校验,则返回的tail节点可能是已经取消的,或者设置状态失败。此情况下需要唤醒该节点,唤醒后该节点会继续执行await( )的部分,从而进入到acquireQueued( )方法,就能清除CANCELLED状态的节点,并将前置节点设置为SINGAL,接着尝试获取锁,获取不到又被阻塞。
因为await( )和singal( )方法在lock和unlock中执行,unlock后一定会唤醒阻塞队列中的节点。
3.5 await被唤醒后
当调用await( )时,线程进入条件队列,线程所在阻塞队列节点被移除,然后进入while后被阻塞。当被唤醒后线程继续从park( )后执行,然后该节点都会从Condition队列转移到Sync队列,并调用acquireQueued( )方法“阻塞式”竞争锁,因此await( )方法返回时,当前线程一定是持有锁的。
但线程被唤醒可能是由于singal( )引起,也可能是中断引起的(但无论那种原因都会执行第一段话的过程),为此我们将线程被唤醒和线程去抢锁看成两个时间点,则可划分为:
1、整个过程,从未发生中断,唤醒由singal( )引起
2、过程中发生了中断:
- 中断发生在singal( )之前,即唤醒由中断引起
- 中断发生在signal之后,此时又有两种情况:
a. 中断发生在singal( )之后,但在获取锁之前(即在刚唤醒后发生中断)
b. 中断发生在singal( )之后,但在获取锁时(即在抢锁过程中发生中断)
讨论之前,我们需要明确,中断机制只是改变了中断标志,并不会立即终止程序运行,它的作用在于对中断信号的处理上,而await( )方法调用的acquireQueued( )方法,该方法并没有处理中断信号,只是对保存了中断信号状态,交由上层函数处理,即这里的await( )。针对出现的情况,在Condition提供了两个属性,来表示三种情况:
0:代表整个过程中一直没有中断发生,唤醒由singal( )引起。
THROW_IE:表示退出await()方法时需要抛出InteruptedException,这种模式对应于中断发生在signal之前。
REINTERRUPT:表示退出await()方法时只需要再自我中断以下, 这种模式对应于中断发生在signal之后, 即中断来的太晚了。
接下来我们针对几种情况做不同讨论:
3.5.1 case1:整个过程未发生中断
1 //检查等待过程中的中断情况 2 private int checkInterruptWhileWaiting(Node node) { 3 //若线程未中断,则返回0 4 return Thread.interrupted() ? 5 //若线程中断,则要判断该中断发生在哪个时间节点 6 (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 7 0; 8 }
这种情况比较简单,Thread.interrupted( )返回false,checkInterruptWhileWaiting( )方法返回0,即interruptMode==0,由于是singal( )唤醒,则线程已经转移至Sync队列,跳出循环。
1 public final void await() throws InterruptedException { 2 if (Thread.interrupted()) 3 throw new InterruptedException(); 4 5 Node node = addConditionWaiter(); 6 int savedState = fullyRelease(node); 7 int interruptMode = 0; 8 9 while (!isOnSyncQueue(node)) { 10 LockSupport.park(this); 11 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 12 break; 13 } 14 15 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 16 interruptMode = REINTERRUPT; 17 if (node.nextWaiter != null) // clean up if cancelled 18 unlinkCancelledWaiters(); 19 if (interruptMode != 0) 20 reportInterruptAfterWait(interruptMode); 21 }
接下来就会执行acquireQueued( )方法,因为整个过程都未发生中断,则acquireQueued( )方法返回false,不会继续执行第一个if,以及后面的两个if。值得注意的是,acquireQueued( )方法中传入的savedState是之前解锁时保留的,即当初解开多少锁,就再次获取多少锁。
3.5.2 case2:中断发生在singal之前
此情况说明该次唤醒是由中断引起的,则之后再调用singal( )就“晚了”(因为该线程已经通过非singal( )方法下进入Sync队列,Condition队列中已经没有该节点或singal( )时的CAS操作节点状态时会失败)。继续看代码:
1 private int checkInterruptWhileWaiting(Node node) { 2 return Thread.interrupted() ? 3 (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 4 0; 5 }
1 //将节点转移至阻塞队列(发生中断时,不是由signal()进行的转移) 2 final boolean transferAfterCancelledWait(Node node) { 3 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { 4 enq(node); 5 return true; 6 } 7 while (!isOnSyncQueue(node)) 8 Thread.yield(); 9 return false; 10 }
此时Thread.interrupted返回true,执行transferAfterCancelledWait( )方法。因为通过signal( )方法,节点从将从Condition队列转移至Sync队列,节点状态会置为0。因为中断唤醒的,所以节点还没有进行转移,所以if中的CAS会执行成功,然后enq( )方法将节点插入Sync队列,返回true。注意此时nextWaiter并没有断开,仍在Condition队列中。
1 while (!isOnSyncQueue(node)) { 2 LockSupport.park(this); 3 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 4 break; 5 } 6 7 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 8 interruptMode = REINTERRUPT; 9 if (node.nextWaiter != null) // clean up if cancelled 10 unlinkCancelledWaiters(); 11 if (interruptMode != 0) 12 reportInterruptAfterWait(interruptMode);
返回true,checkInterruptWhileWaiting( )方法即会返回THROW_IE==-1,跳出循环。回到await( ),当前线程执行acquireQueued( )方法“阻塞式”获取锁,因为发生了中断该方法最终会返回true,但因为继续不会执行第一个if。因为nextWaiter并未断开,则执行第二个if,通过unlinkCancelledWaiters( )方法断开所有不是CONDITION状态的节点,即将当前节点从Condition队列中移除。
1 private void reportInterruptAfterWait(int interruptMode) 2 throws InterruptedException { 3 //值为THROW_IE抛出异常 4 if (interruptMode == THROW_IE) 5 throw new InterruptedException(); 6 //值为REINTERRUPT,再自己进行一次中断 7 else if (interruptMode == REINTERRUPT) 8 selfInterrupt(); 9 }
接着执行第三个if,这里对两种情况的处理是,值为THROW_IE抛出异常;值为REINTERRUPT,再自己进行一次中断,这个处理方式和acquie( )方法对执行acquireQueued( )时发生中断时的处理方式相同。
3.5.3 case3:中断发生在singal之后
这种情况下有两种情况:
(1)中断发生在singal( )之后,但在acquireQueued( )之前发生
1 private int checkInterruptWhileWaiting(Node node) { 2 return Thread.interrupted() ? 3 (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 4 0; 5 }
1 final boolean transferAfterCancelledWait(Node node) { 2 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { 3 enq(node); 4 return true; 5 } 6 while (!isOnSyncQueue(node)) 7 Thread.yield(); 8 return false; 9 }
此情况下Thread.interrupted( )返回true,执行transferAfterCancelledWait( ),因为在中断前执行了singal( ),所以不会执行if,最终返回false。这里的while是因为singal( )方法是另一个线程执行的,所以可能singal( )方法才改变节点状态,还没有将节点加入Sync队列,因此需要自旋等待,直到该节点加入阻塞队列。
1 while (!isOnSyncQueue(node)) { 2 LockSupport.park(this); 3 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 4 break; 5 } 6 7 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 8 interruptMode = REINTERRUPT; 9 if (node.nextWaiter != null) // clean up if cancelled 10 unlinkCancelledWaiters(); 11 if (interruptMode != 0) 12 reportInterruptAfterWait(interruptMode);
回到await( ),最终interruptMode为REINTERRUPT,跳出循环。因为acquireQueued( )方法中没有产生中断,则会返回false,不会再执行第一个if。转移操作是由singal( )进行的,也不会执行第二个if。最后根据interruptMode的值,线程自己再执行了一次中断操作。
(2)中断发生在singal( )之后,是在acquireQueued( )方法中发生的中断。
此种情况下,跳出循环时,interruptMode值为0。但在acquireQueued( )中发生了中断,所以返回true,会接着执行第一个if,将interruptMode的值赋为REINTERRUPT,接着执行与(1)相同的操作。
3.6 await方法小结
这是await( )的大致执行流程,这里对中断产生的唤醒也去进行了抢锁操作,但实际上此时即使抢到锁,也会发现条件并不满足,所以还是再次进入await( ),这样就会浪费资源。这种情况我们可以选择使用awaitUninterruptibly( )方法,该方法会忽略中断,只有接受singal( )方法的唤醒。