ReentrantLock Condition 线程间通信
Posted xieyanke
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock Condition 线程间通信相关的知识,希望对你有一定的参考价值。
方法介绍:
boolean await() 阻塞线程 直至被唤醒
boolean await(long time, TimeUnit unit) 阻塞线程 超时自动唤醒
void signal() 唤醒一个await线程
void signalAll() 唤醒所有await线程
场景举例:
班级组织郊游, 每个到了的同学就在原地等待上大巴,先到的排在最前面, 后到的紧接着排在最后一个人后面, 直到班长来了, 班长有两种方式让让同学上车,
方式一: 班长从前往后一个一个点 点到几个上几个(按排队顺序) ------- await() + signal() 的使用
方式二: 班长直接让所有人按排队顺序上车 ----- await() + signalAll() 的使用
方式一代码如下:
public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock(); //new condition Condition condition = lock.newCondition(); //t1线程 Thread t1 = new Thread(()->{ try { lock.lock(); System.out.println("t1到位准备 等待班长点到"); condition.await(); System.out.println("t1已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t2 = new Thread(()->{ try { lock.lock(); System.out.println("t2到位准备 等待班长点到"); condition.await(); System.out.println("t2已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t3 = new Thread(()->{ try { lock.lock(); System.out.println("t3到位准备 等待班长点到"); condition.await(); System.out.println("t3已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t4 = new Thread(()->{ try { lock.lock(); System.out.println("班长到位 准备按排队顺序点到上车"); for (int i=1; i<=2; i++){ System.out.println("点"+i+"个"); condition.signal(); System.out.println("休息1秒...."); Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); }finally { System.out.println("点到的同学可以上车了"); lock.unlock(); } }); t1.start(); //休眠200秒是为了保证t1在t2之前执行线程 Thread.sleep(200); t2.start(); //休眠200秒是为了保证t2在t3之前执行线程 Thread.sleep(200); t3.start(); //休眠200秒是为了保证t3在t4之前执行线程 t4.start(); } 打印结果: t1到位准备 等待班长发令 t2到位准备 等待班长发令 t3到位准备 等待班长发令 班长到位准备发令上车 点1个 休息1秒.... 点2个 休息1秒.... 点到的同学可以上车了 t1已上车 t2已上车
从打印结果可以看出, t1 和 t2同学被点到 并且已上车 , t3则需要等待班长继续点到
方式二代码如下:
public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock(); //new condition Condition condition = lock.newCondition(); //t1线程 Thread t1 = new Thread(()->{ try { lock.lock(); System.out.println("t1到位准备 等待班长点到"); condition.await(); System.out.println("t1已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t2 = new Thread(()->{ try { lock.lock(); System.out.println("t2到位准备 等待班长点到"); condition.await(); System.out.println("t2已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t3 = new Thread(()->{ try { lock.lock(); System.out.println("t3到位准备 等待班长点到"); condition.await(); System.out.println("t3已上车"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); Thread t4 = new Thread(()->{ try { lock.lock(); //发令让所有同学上车 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); }finally { System.out.println("所有同学可以上车了"); lock.unlock(); } }); t1.start(); //休眠200秒是为了保证t1在t2之前执行线程 Thread.sleep(200); t2.start(); //休眠200秒是为了保证t2在t3之前执行线程 Thread.sleep(200); t3.start(); //休眠200秒是为了保证t3在t4之前执行线程 t4.start(); } 打印结果: t1到位准备 等待班长点到 t2到位准备 等待班长点到 t3到位准备 等待班长点到 所有同学可以上车了 t1已上车 t2已上车 t3已上车
从打印结果可以看出,班长发令所有人上车之后, 所有人按当时排队顺序陆续上车了
数据结构代码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //头节点 private transient volatile Node head; //尾部节点 private transient volatile Node tail; //状态 private volatile int state; //当前拥有锁的线程 private transient Thread exclusiveOwnerThread; static final class Node { //节点等待状态 这个很重要 relase时会根据节点等待状态做出下一步动作 // 1取消由于超时或打断而取消 不再阻塞 // -1 等待被唤醒 后续节点需要阻塞 // -2 等待转移 非阻塞 // -3 共享参数 需要继续传播给下一个节点 volatile int waitStatus; //前节点 volatile Node prev; //后节点 volatile Node next; //节点线程 这个是重点 保存了等待锁的线程 volatile Thread thread; //下一个等待锁的节点 condition时有用 本文关注重点 Node nextWaiter; }
//本文关注重点 public class ConditionObject implements Condition, java.io.Serializable { //condition 阻塞队列第一个线程节点 private transient Node firstWaiter; //condition 阻塞队列最后一个线程节点 private transient Node lastWaiter; } }
await():数据结构变化
lock.newCondition() 实际是新建了一个ConditionObject对象, (这里涉及到一些内部类引用外部类 具体原理不详述)
先从数据结构看await()工作原理, 方便后面源码分析
1 new newCondition() 此时ConditionObject中属性 firstWaiter和lastWaiter都为null,
2 线程t1调用代码 condition.await(),此时设置firstWaiter = lastWaiter = new Node(t1)
3 线程t2调用代码 condition.await(),此时设置 lastWaiter = new Node(t2), 并且 firstWaiter.nextWaiter = node(t2)
4 线程t3原理同上一步
sign():数据结构变化
下图中有两个框,
第一个框表示t1 t2 t3线程中执行了lock.lock() 以及 condition.await() , t5 t6只执行了lock.lock() 之后定格的数据结构状态图
第二个框表示新开线程执行lock.lock() 以及 condition.sign()后的定格数据结构状态图(signAll()原理一样, 不同点是会把所有condition的waiter节点全部移入AQS的CLH队列中)
简述condition使用过程逻辑
1 新建一个ReentrantLock对象
2 新建一个Condition(ReentrantLock内部类) 对象
3 await() 在condition中添加一个waiter节点,链式结构不详述 firstWaiter- > 若干node -> lastWaiter
循环判断是否当前新建节点是否transfer到CLH队列中,多线程情况下,当前节点可能正好被其他线程移入CLH中了
4 sign() 将firstWaiter移入CLH最后一个节点(如果CLH最后一个节点为cancel 则直接传播给当前节点)
源码分析:
await()
public final void await() throws InterruptedException { //如果当前线程被打断 抛出异常 if (Thread.interrupted()) throw new InterruptedException(); //新增一个等待节点 Node node = addConditionWaiter(); //设置AQS独占线程为空, 释放当前线程持有AQS锁(如果AQS阻塞队列有节点则传播给下一个节点) int savedState = fullyRelease(node); int interruptMode = 0; //判断是否在阻塞队列 while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //代码运行到这必然是别的线程调用sign()方法, 已经将waiter队列中的节点移入到CLH(其实上面isOnSyncQueue(node)=true就表示移入了) //当前node设置为head节点 等待lock.unlock()让出锁给CLH队列的下一个节点 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } private Node addConditionWaiter() { //找到最后一个节点 Node t = lastWaiter; //如果最后一个节点状态发生改变 已非等待唤醒状态 则从队列中剔除 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } //新建一个节点(当前线程, waitStatus=-3等待唤醒状态) Node node = new Node(Thread.currentThread(), Node.CONDITION); //如果当前没有等待节点 则设置此新建节点为第一个等待节点 if (t == null) firstWaiter = node; //如果如果已经存在等待节点 则当前新建节点挂在最后一个等待节点后面 else t.nextWaiter = node; //最后一个节点即为当前新增节点 lastWaiter = node; return node; }
sign()
//唤醒await中的线程 public final void signal() { //判断当前线程是否为AQS锁独占线程 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) //唤醒第一个waiter节点 doSignal(first); } //以下代码的作用是转移第一个节点到CLH队列 private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); } //转移过程 这段代码是核心代码 final boolean transferForSignal(Node node) { //原子操作 修改node等待状态从-3修改为0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //将此等待节点加入到CLH的tail节点之后 成为新的tail节点 //节点p为node的前一个节点 也就是加入之前CLH的tail节点 Node p = enq(node); int ws = p.waitStatus; //如果p节点为取消状态 或者 p节点为非取消状态但是修改p为独占状态失败 则唤醒当前节点线程 //因为链式调用的原则 node线程的唤醒交给前驱节点, 若前驱节点无法达到唤醒当前节点条件 那么就在此处直接唤醒 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
condition还有一些其他的await方法, 实现方法基本差不多, 只不过在LockSupport.park时带上线程阻塞时间
以上是关于ReentrantLock Condition 线程间通信的主要内容,如果未能解决你的问题,请参考以下文章
ReentrantLock与Condition构造有界缓存队列与数据栈
从使用角度看 ReentrantLock 和 Condition
Java ReEntrantLock 之 Condition条件(Java代码实战-002)