• JUC.Condition学习笔记[附详细源码解析]


    目录

    Condition的概念

    大体实现流程

      I.初始化状态
      II.await()操作
      III.signal()操作

    3个主要方法

      Condition的数据结构
      线程何时阻塞和释放
      await()方法
      signal()和signalAll()方法

    Condition示例:生产者和消费者


    JUC提供了Lock可以方便的进行锁操作,但是有时候我们也需要对线程进行条件性的阻塞和唤醒,这时我们就需要condition条件变量,它就像是在线程上加了多个开关,可以方便的对持有锁的线程进行阻塞和唤醒。

    Condition的概念

    Condition主要是为了在J.U.C框架中提供和Java传统的监视器风格的wait,notify和notifyAll方法类似的功能。
     
    JDK的官方解释如下:
    条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
    Condition实质上是被绑定到一个锁上。
     
    JUC锁机制(Lock)学习笔记中,我们了解到AQS有一个队列,同样Condition也有一个等待队列,两者是相对独立的队列,因此一个Lock可以有多个Condition,Lock(AQS)的队列主要是阻塞线程的,而Condition的队列也是阻塞线程,但是它是有阻塞和通知解除阻塞的功能
    Condition阻塞时会释放Lock的锁,阻塞流程请看下面的Condition的await()方法。

    大体实现流程

    AQS等待队列与Condition队列是两个相互独立的队列
    await()就是在当前线程持有锁的基础上释放锁资源,并新建Condition节点加入到Condition的队列尾部,阻塞当前线程
    signal()就是将Condition的头节点移动到AQS等待节点尾部,让其等待再次获取锁
     
    以下是AQS队列和Condition队列的出入结点的示意图,可以通过这几张图看出线程结点在两个队列中的出入关系和条件。
     
    I.初始化状态:AQS等待队列有3个Node,Condition队列有1个Node(也有可能1个都没有)
    II.节点1执行Condition.await()
    1.将head后移
    2.释放节点1的锁并从AQS等待队列中移除
    3.将节点1加入到Condition的等待队列中
    4.更新lastWaiter为节点1
    III.节点2执行signal()操作
    5.将firstWaiter后移
    6.将节点4移出Condition队列
    7.将节点4加入到AQS的等待队列中去
    8.更新AQS的等待队列的tail

    3个主要方法

    Condition的数据结构

    我们知道一个Condition可以在多个地方被await(),那么就需要一个FIFO的结构将这些Condition串联起来,然后根据需要唤醒一个或者多个(通常是所有)。所以在Condition内部就需要一个FIFO的队列。
    private transient Node firstWaiter;
    private transient Node lastWaiter;
    上面的两个节点就是描述一个FIFO的队列。我们再结合前面提到的节点(Node)数据结构。我们就发现Node.nextWaiter就派上用场了!nextWaiter就是将一系列的Condition.await*串联起来组成一个FIFO的队列。

    线程何时阻塞和释放

    阻塞:await()方法中,在线程释放锁资源之后,如果节点不在AQS等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
    释放:signal()后,节点会从condition队列移动到AQS等待队列,则进入正常锁的获取流程

    await方法

    ReentrantLock是独占锁,一个线程拿到锁后如果不释放,那么另外一个线程肯定是拿不到锁,所以在lock.lock()和lock.unlock()之间可能有一次释放锁的操作(同样也必然还有一次获取锁的操作)。在进入lock.lock()后唯一可能释放锁的操作就是await()了。也就是说await()操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!
     
     Java Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
     
    public final void await() throws InterruptedException {
        
    // 1.如果当前线程被中断,则抛出中断异常
        if (Thread.interrupted())
            
    throw new InterruptedException();
        
    // 2.将节点加入到Condition队列中去,这里如果lastWaiter是cancel状态,那么会把它踢出Condition队列。
        Node node = addConditionWaiter();
        
    // 3.调用tryRelease,释放当前线程的锁
        long savedState = fullyRelease(node);
        
    int interruptMode = 0;
        
    // 4.为什么会有在AQS的等待队列的判断?
        // 解答:
    signal操作会将Node从Condition队列中拿出并且放入到等待队列中去,在不在AQS等待队列就看signal是否执行了
        // 如果不在AQS等待队列中,就park当前线程,如果在,就退出循环,这个时候如果被中断,那么就退出循环
        while (!isOnSyncQueue(node)) {
            LockSupport.park(
    this);
            
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                
    break;
        }
        
    // 5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了4中的while循环
        //
    自旋等待尝试再次获取锁,调用acquireQueued方法
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        
    if (node.nextWaiter != null)
            unlinkCancelledWaiters();
        
    if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
     
    整个await的过程如下:
      1.将当前线程加入Condition锁队列。特别说明的是,这里不同于AQS的队列,这里进入的是Condition的FIFO队列。进行2。
      2.释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。进行3。
      3.自旋(while)挂起,直到被唤醒或者超时或者CACELLED等。进行4。
      4.获取锁(acquireQueued)。并将自己从Condition的FIFO队列中释放,表明自己不再需要锁(我已经拿到锁了)。
    可以看到,这个await的操作过程和Object.wait()方法是一样,只不过await()采用了Condition队列的方式实现了Object.wait()的功能。

    signal和signalAll方法

    await*()清楚了,现在再来看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要将Condition.await*()中FIFO队列中第一个Node唤醒(或者全部Node)唤醒。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。 

     Java Code 
    1
    2
    3
    4
    5
    6
    7
     
    public final void signal() {
        
    if (!isHeldExclusively())
            
    throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        
    if (first != null)
            doSignal(first);
    }

    这里先判断当前线程是否持有锁,如果没有持有,则抛出异常,然后判断整个condition队列是否为空,不为空则调用doSignal方法来唤醒线程,看看doSignal方法都干了一些什么:

     Java Code 
    1
    2
    3
    4
    5
    6
    7
    8
     
    private void doSignal(Node first) {
        
    do {
            
    if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } 
    while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    上面的代码很容易看出来,signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程。当然了遇到CANCELLED线程就需要将其从FIFO队列中剔除。
     
     Java Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    final boolean transferForSignal(Node node) {
        
    /*
         * 设置node的waitStatus:Condition->0
         */

        
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            
    return false;

        
    /*
         * 加入到AQS的等待队列,让节点继续获取锁
         * 设置前置节点状态为SIGNAL
         */
        Node p = enq(node);
        
    int c = p.waitStatus;
        
    if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        
    return true;
    }
     
    上面就是唤醒一个await*()线程的过程,根据前面的介绍,如果要unpark线程,并使线程拿到锁,那么就需要线程节点进入AQS的队列。所以可以看到在LockSupport.unpark之前调用了enq(node)操作,将当前节点加入到AQS队列。
     
    signalAll和signal方法类似,主要的不同在于它不是调用doSignal方法,而是调用doSignalAll方法:
     Java Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter  = null;
        
    do {
            Node next = first.nextWaiter;
            first.nextWaiter = null;
            transferForSignal(first);
            first = next;
        } 
    while (first != null);
    }

    这个方法就相当于把Condition队列中的所有Node全部取出插入到等待队列中去。

    Condition应用示例:生产者和消费者

    Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。在最后我们来看一个应用示例

     Java Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
     
    /**
     * 生产者、消费者示例
     */

    public class ConditionTest {
        
    private int storage;
        
    private int putCounter;
        
    private int getCounter;
        
    private Lock lock = new ReentrantLock();
        
    private Condition putCondition = lock.newCondition();
        
    private Condition getCondition = lock.newCondition();

        
    public void put() throws InterruptedException {
            
    try {
                lock.lock();
                
    if (storage > 0) {
                    putCondition.await();
                }
                storage++;
                System.out.println(
    "put => " + ++putCounter );
                getCondition.signal();
            } 
    finally {
                lock.unlock();
            }
        }

        
    public void get() throws InterruptedException {
            
    try {
                lock.lock();
                lock.lock();
                
    if (storage <= 0) {
                    getCondition.await();
                }
                storage--;
                System.out.println(
    "get  => " + ++getCounter);
                putCondition.signal();
            } 
    finally {
                lock.unlock();
                lock.unlock();
            }
        }

        
    public class PutThread extends Thread {
            @Override
            
    public void run() {
                
    for (int i = 0; i < 100; i++) {
                    
    try {
                        put();
                    } 
    catch (InterruptedException e) {
                    }
                }
            }
        }

        
    public class GetThread extends Thread {
            @Override
            
    public void run() {
                
    for (int i = 0; i < 100; i++) {
                    
    try {
                        get();
                    } 
    catch (InterruptedException e) {
                    }
                }
            }
        }

        
    public static void main(String[] args) {
            
    final ConditionTest test = new ConditionTest();
            Thread put = test.
    new PutThread();
            Thread get = test.
    new GetThread();
            put.start();
            get.start();
        }
     
     
    原创文章,请注明引用来源:CM4J
    参考文章列表:
  • 相关阅读:
    springboot mybatis-plus 多数据源
    怎样建网站?
    哪些软件外包公司为什么不赚钱?
    初高中英语 牛津书虫全套系列 英汉双语读物系列1-6级 音频+文本网盘下载【收藏】
    英语零基础直达六级水平-英语学习全能套装视频完整版【收藏】
    重构:改善既有代码的设计(第2版)(pdf,epub,azw3,mobi)[收藏]
    彻底关闭浏览器的弹出广告或通知(Chrome+Firefox )
    《读者》杂志38年合集 (1981-2018)电子版【收藏】
    怎样在知乎赚钱?(知乎好物)
    《故事会》(2010-2019)全年合集pdf版 [收藏]
  • 原文地址:https://www.cnblogs.com/cm4j/p/juc_condition.html
Copyright © 2020-2023  润新知