并发编程-Condition

并发编程-Condition

本篇主要讲述的内容是线程之间的通信——Conditon ,那什么是线程通信?线程之间的通信是指当某个线程修改了一个对象的值时,另外一个线程能够感知到该值的变化并进行相应的操作。实现线程之间通信的方法有以下几种

  1. 基于volatile修饰共享变量
  2. 通过wait/notify机制
  3. Thread.join方法
  4. 使用 synchronized 关键字
  5. Conditon 中的await/signal 方法

在介绍Conditon 之前我们先来认识一下wait和notify

wait/notify

wait()和 notify()是 Java 提供的两个方法,它们属于 Object类,因此所有的对象都可以使用这两个方法。这两个方法主要用于线程间的通信和协调,是实现多线程同步的重要手段。

  1. wait() 方法
    • 当一个线程需要等待某个条件成立才能继续执行时,它可以调用对象的wait()方法。
    • 调用wait()方法的线程会被放入对象的等待队列中,等待其他线程调用同一对象的notify() 或notifyAll() 方法。
    • 当前线程在等待期间会释放对象的锁,这样其他线程可以获取该对象的锁并修改其状态。
    • wait()方法会抛出InterruptedException异常,所以需要使用 try-catch 块来处理。
  2. notify() 方法
    • 当一个线程修改了对象的状态,并希望唤醒等待该对象的等待队列中的一个线程时,它可以调用对象的notify() 方法。
    • 被唤醒的线程将从等待队列中移除,并从对象锁中获得执行机会。
    • 注意,notify() 只唤醒一个等待的线程,如果有多个线程在等待,则随机选择一个唤醒。如果想要唤醒所有等待的线程,应该使用 notifyAll()方法。

使用

wait/notify 方法实际上时针对同一共享对象的竞争来实现数据变更的通知,也就是当某个共享变量满足某种条件时会触发唤醒和阻塞,从而实现线程的通信。所以 wait/notify更适用于生产者和消费者的场景,接下来我们就用wait/notify来模拟生产者和消费者的使用场景。

//缓冲
public class Buffer {
    private Queue<Integer> queue;
    private int maxSize;
    
    public Buffer(int maxSize) {
        this.maxSize = maxSize;
        queue = new LinkedList<>();
    }
    
    public synchronized void put(int value) throws InterruptedException {
        while (queue.size() == maxSize) {
            wait();
        }
        queue.add(value);
        notify();
    }

    public synchronized Integer take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        notify();
        return queue.poll();
    }
}

//生产者
public class Producer implements Runnable {
    private Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {
            try {
                System.out.println("生产者 生产消息: " + i);
                TimeUnit.SECONDS.sleep(1);
                buffer.put(i++);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//消费者
public class Consumer implements Runnable{
    private Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Integer value = buffer.take();
                System.out.println("消费者 消费消息: " + value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//测试类
public class TestExample {
    public static void main(String[] args) throws InterruptedException {
        Buffer buffer = new Buffer(8);
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}
  1. 在上述代码中,我们定义了Queue作为缓冲区,并使用synchronized关键字来确保线程安全。
  2. 当queue.size() == maxSize时,生产者调用wait()方法释放锁并进入等待状态。当queue.size() != maxSize时(即消费者从缓冲区中取出了一个消息),它会调用notify()唤醒等待的生产者。
  3. 当queue.isEmpty() == true时,消费者调用wait()方法释放锁并进入等待状态。当queue.isEmpty() == false 时(即生产者向缓冲区中放入了一个消息),它会调用notify()唤醒等待的消费者。

整个过程图来展示的话如下图所示
在这里插入图片描述

原理

结合代码和功能描述我们来梳理一下wait()/notify()的原理。

  1. 线程间的通信必然涉及到锁且wait()/notify()方法依赖于synchronized锁

  2. 调用wait()方法的线程会阻塞,假设有多个线程同时调用wait()方法,那这些线程会阻塞在哪里?换句话说阻塞的线程怎么存储。使用synchronized关键字抢占锁失败会进入一个同步队列。那基于这个思路,我们可以大致猜出这些阻塞的线程基本上也是放入一个等待队列中

  3. 当调用notify() 方法时,之前再一个队列中阻塞的线程会移动到synchronized的同步队列并唤醒,整个过程我们通过一个图来简单描述一下
    在这里插入图片描述

补充

在文章的开头我们提到了Thread.join()方法也能实现线程间的通信,我们从源码层面可以一探究竟。

public final void join() throws InterruptedException {
        join(0);
}
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
}

从源码中我们可以清楚的看到join方法的本质就是wait()和notify()。

Conditon

在了解了wait()/notify()的基本原理之后,我们正式进入Condition的解析。Condition本身的作用和wait()/notify()的作用相同,都是基于条件去唤醒和阻塞。就相当于用java重新写了一个wait()/notify()。所以它们的大致思想是相通的,有了这个前提我们理解起来就会清晰明了很多。

使用

Conditon的使用也很简单,我们还是拿生产者消费者的例子改造一下

public class Buffer {
    private Queue<Integer> queue;
    private int maxSize;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public Buffer(int maxSize) {
        this.maxSize = maxSize;
        queue = new LinkedList<>();
    }

    public  void put(int value) {
        try {
            lock.lock();
            while (queue.size() == maxSize) {
                condition.await();
            }
            queue.add(value);
            condition.signal();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }

    public  Integer take()  {
        try {
            lock.lock();
            while (queue.isEmpty()) {
                condition.await();
            }
            condition.signal();
            return queue.poll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return null;
    }
}
//生产者
public class Producer implements Runnable {
    private Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {
            try {
                System.out.println("生产者 生产消息: " + i);
                TimeUnit.SECONDS.sleep(1);
                buffer.put(i++);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//消费者
public class Consumer implements Runnable{
    private Buffer buffer;
    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Integer value = buffer.take();
                System.out.println("消费者 消费消息: " + value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
//main方法
public static void main(String[] args) throws InterruptedException {
        Buffer buffer = new Buffer(5);
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
}

运行结果
在这里插入图片描述

需求分析

既然Conditon的作用与wait()/notify()的作用相同,那我们不妨来推导一下Conditon做了什么

  1. 实现了线程的阻塞和唤醒。

  2. 它是通过await()和signal()或signalAll() 来实现的阻塞与唤醒

    await() : 使线程阻塞并释放锁

    signal(): 唤醒阻塞的线程

  3. Condition 的使用前提是基于Lock 加锁,加锁的话我们很容易联想到AQS队列

  4. 当调用await() 释放锁的时候,这个释放锁的线程肯定不会进入AQS队列,当调用signal()方法时又去唤醒阻塞的线程。那么问题来了await()释放的线程阻塞在哪里,signal()又是从哪唤醒的。我们通过对wait()/notify()的了解既然它们是等价的,那是不是会有一个区别于AQS队列之外的数据结构来存储阻塞的线程。signal()方法唤醒的时候也是从这个数据结构中去唤醒。

  5. 唤醒之后的线程干什么,应该去抢占锁,但它是基于Lock中AQS的机制来抢占锁,所以唤醒之后的线程应该加入到AQS队列中。

类图

在这里插入图片描述

源码分析

有了上面的分析之后我们正式开始源码的解析,Condition的主要方法就两个await() 和signal()。我们先从await()入手。

await()

那await()又做了哪些事情

  1. 调用await()方法的线程要释放锁
  2. 释放锁的线程要阻塞并且存储到一个数据结构中(队列)可以简称为Condition队列
  3. 当唤醒之后需要重新抢占锁
  4. 处理interrupt()的中断响应

有了上述几点梳理之后,我们再来看await()源码,我们先从大的层面来看

public final void await() throws InterruptedException {
    //Thread.interrupted() 为ture直接抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //构建一个状态为Condition的节点,所以这里采用的数据结构仍然是链表。这里就是把当前线程加入到Condition队列中
    Node node = addConditionWaiter();
    //fullyRelease直译过来可以理解为完全的释放锁(这里这么做是考虑有重入的情况)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //判断构建的node节点在不在同步队列中(即AQS队列)为false则阻塞当前线程
    while (!isOnSyncQueue(node)) {
        //阻塞当前线程
        LockSupport.park(this);
        //判断当前被阻塞的线程是不是因为interrupted()方法唤醒的
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //被阻塞的线程去唤醒时尝试获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    //处理中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

在上述的await()源码分析中我们发现整体逻辑跟我们分析要点一致,接下来我们着手细节,逐步分析

addConditionWaiter()
//从整体上看 Condition中维护了一个等待队列 我们称之为Condition队列,从源码中大致可以看出这个队列是个单向链表
private Node addConditionWaiter() {
    //定义一个尾节点
    Node t = lastWaiter;
    // 如果尾节点不为空则队列不为空并且 节点状态不等于Node.CONDITION 时清理失效节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //第一次构建时 t == null,把当前线程封装成Node且状态为CONDITION并赋值为头节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        //如果不为空则加入到队尾
        t.nextWaiter = node;
    //若t == null 则当前这个节点既是头节点也是尾节点,若t!=null 则把刚加入链表中的节点作为尾节点,这里链表采用的是尾插法
    lastWaiter = node;
    return node;
}
fullyRelease(node)
//该方法的主要作用就是释放锁
final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            //获取state的值
            int savedState = getState();
            //release(savedState) 方法就是释放锁的方法,在上一篇讲述Lock时已经有详细解释,这里就不作过多描述了
            if (release(savedState)) {
                failed = false;
                //完全释放锁savedState == 0
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            //如果failed == true 那么这个节点的状态就是CANCELLED,这就跟addConditionWaiter()方法中清理失效节点                   //对应起来了
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
}
isOnSyncQueue(node)
//判断当前节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        return findNodeFromTail(node);
}
//findNodeFromTail字面意思就是从尾部开始遍历查找,整体比较简单
private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            //在队列中找到该节点返回true
            if (t == node)
                return true;
            //没有找到返回false
            if (t == null)
                return false;
            t = t.prev;
        }
}

这里强调一下isOnSyncQueue()是判断当前节点是否在AQS队列中,由于fullyRelease()已经释放了锁,所以第一次判断的结果是false。则去阻塞当前线程

while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

await()源码整体上相对简单,接下来我们来看signal()方法

signal()

signal() 方法做了什么

  1. 将阻塞的线程唤醒
  2. 把唤醒后的线程从Condition队列移动到AQS队列中

理清了这两点之后我们再来看signal()的源码

public final void signal() {
     //isHeldExclusively()是判断当前线程是不是获得锁的线程,因为signal()调用的前提是获得锁。
     //如果isHeldExclusively()==false,则直接抛出异常
     if (!isHeldExclusively())
         throw new IllegalMonitorStateException();
    //获取到队列的头节点,因为Condition队列是个单向链表,所以有了头节点就能找到整个链表
     Node first = firstWaiter;
     if (first != null)
         //如果头节点不为空 调用doSignal(first)方法,很明显这个方法是唤醒阻塞的线程和队列转移的具体的执行
         doSignal(first);
 }
//doSignal()方法里面是个do-while循环,do{}里面我们可以看到是操作链表的指向,也就是说while()里边的条件才是唤醒和转移队列
//transferForSignal()是重点方法
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) {
    //通过CAS操作修改Node的状态,由CONDITION修改为0,如果不成功则该节点需要取消
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //看到enq这个方法我们会非常的眼熟,AQS中获取锁失败时也是调用这个方法加入等待队列中,这里就完成了从Condition队列到等待队列的转移
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果ws > 0 或者通过CAS修改状态失败唤醒线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

这个地方有的小伙伴可能对唤醒的条件有疑问,既然加入了AQS队列了那直接等着触发锁竞争的机制不就能唤醒,为什么满足ws > 0或者compareAndSetWaitStatus(p, ws, Node.SIGNAL) == false其中一个就直接唤醒了。我们先来屡一下条件

  1. 因为我们通过enq(node)得到返回的p节点是原来AQS队列的尾节点,那ws>0表示原来的尾节点的状态是CANCELLED
  2. compareAndSetWaitStatus(p, ws, Node.SIGNAL) 修改原来AQS队列的尾节点的状态为SINGAL ,如果结果为false 实际上不会唤醒任何正在等待的线程。这是一种回退策略,确保即使无法原子性地更新等待状态,线程仍然能够被正确地唤醒并继续执行。这样做的目的是为了提高并发程序的健壮性,防止线程因为无法获取所需的资源而永久地被阻塞。所以为了避免死锁直接调用LockSupport.unpark(node.thread)唤醒节点上的线程

众所周知,唤醒之后的线程会回到调用await()方法里面 LockSupport.park()的位置,我们再来看一段代码

while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    //此时checkInterruptWhileWaiting(node) == 0.不是因为中断被唤醒的所以跳出循环去acquireQueued() 抢占锁
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)

由此我们可以推断出为什么ws>0这个条件成立时就去唤醒node.thread,是因为上一个节点是CANCELLED的话,AQS队列会清理失效的节点,当节点从Condition队列移动到AQS队列不用等待清理动作完成就可直接进行唤醒,一定程度上优化了性能。

再回到await()

调用了signal()方法后被唤醒的线程会在之前阻塞的位置继续执行,之前我们粗略的说了一下checkInterruptWhileWaiting(node)这个方法是判断被唤醒的线程是不是因为interrupt()方法唤醒的,它具体做了什么我们没有细说,那接下来我们来具体聊一聊这个方法

checkInterruptWhileWaiting(node)
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}

在解析这段代码之前,我们先来看一下THROW_IE 和 REINTERRUPT这两个常量代表什么意思

/** Mode meaning to reinterrupt on exit from wait */
//通过注释大致翻译为结束时重新中断
private static final int REINTERRUPT =  1;
/** Mode meaning to throw InterruptedException on exit from wait */
//通过注释大致翻译为结束时抛出InterruptedException异常
private static final int THROW_IE    = -1;
  1. 当Thread.interrupted() == true 代表被中断过,则调用transferAfterCancelledWait(node)方法。
  2. 当Thread.interrupted() == false 代表没有被中段,直接返回0,继续进入while(!isOnSyncQueue(node)) 这个循环判断,这里又是通过调用signal() 方法,此时节点由Condition队列转移到了AQS对队列,所以isOnSyncQueue(node) == true 直接跳出循环
  3. 如果transferAfterCancelledWait(node) == true 返回-1则是抛出中断异常,反之返回1 重新中断

我们再来看transferAfterCancelledWait(node) 这个方法

transferAfterCancelledWait(node)
final boolean transferAfterCancelledWait(Node node) {   
    //因为这是通过Thread.interrupted() == true才执行的方法,如果CAS能成功则证明线程中断的时候还没有调用signal()方法
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}
  1. 如果 compareAndSetWaitStatus(node, Node.CONDITION, 0) == true 则证明还没有调用 signal() 方法线程就已经中断了,那把包含该线程的节点直接加入到AQS队列等待锁的抢占。
  2. 如果compareAndSetWaitStatus(node, Node.CONDITION, 0) == false 则证明线程的中断是在signal()方法调用之后发生的,判断包含该节点的线程是否在AQS队列中,如果不在,调用yield()方法让出CPU时间片,让其他线程执行。

源码分析到这里我们就把await() 到 signal() 执行流程就都串联起来了,我们再把目光聚焦到await() 方法中来

public final void await() throws InterruptedException {
    //........省略部分代码
    //await() 方法中处理中断的这部分逻辑就变得清晰了
    //唤醒之后先让当前线程去尝试获得锁,抢占锁成功,继续执行后续代码
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //清理cancelled节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    //根据interruptMode来判断是抛异常还是重新触发一次中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

总结

本篇我们先是通过wait()/notify()来了解了线程通信的机制。因为wait()/notify()是JVM层面控制的,我们无法窥探它的源码。好在J.U.C中提供了Condition,可以把它理解为wait()/notify()的平替,但它们的原理是相通的,都是在持有同一把锁的基础上,通过线程的阻塞和唤醒来实现的线程通信。Condition在我们日常开发中基本不会用到,但它更多会出现在其他中间件的源码中,所以了解它的核心原理也是有必要的。

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值