并发编程-Condition源码分析&基于Condition实现阻塞队列

并发编程-Condition源码分析&基于Condition实现阻塞队列

上一篇说猜测了condition的实现原理,本篇对condition源码进行剖析,并且来使用condition实现一个阻塞队列,同时聊聊有哪些东西的底层使用了condition。So Run。。。

上篇回顾

主要是这两个方法

【await】:可以阻塞同一把锁上的N个线程、释放锁 

【signal】:唤醒一个等待在用一把锁上的线程

如何让线程等待:有一个等待队列来存储等待中的线程

唤醒等待的线程:condition的等待队列,和aqs中的同步队列是并行的,会牵扯到AQS中同步队列和Condition中的等待队列的转移,那是如何做的呢。

源码分析

await:

  • 释放锁
  • 释放锁的线程应该被阻塞
  • 被阻塞后需要存储在队列中
  • 需要重新竞争锁
  • 要能够处理interrupt的中断响应
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加到等待队列
    Node node = addConditionWaiter();
    //完整的释放锁(考虑到重入锁的问题)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
  //如果当前节点不在aqs同步队列上,因为只有确定当前的线程不在aqs队列中咱们才去阻塞
    while (!isOnSyncQueue(node)) {
        //阻塞当前线程,**注意当其他线程调用signal()时候,当前线程会从这里进行执行,因为上下文切换会保存当前程序的寄存器和程序计数器
        LockSupport.park(this);
        //判断当前被阻塞的线程是否是因为interrupt而唤醒的
        //->因为interrupt中断的操作,会唤醒处于等待的线程,所以这里有可能不是被signal唤醒的而是被interrupt唤醒的
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 重新竞争锁 savedState这个表示的被释放锁的重入次数
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

 

这里构建condition队列,并把当前的线程添加进队列中,想象一下,

  • 现在有线程A、B来抢占线程,这个时候线程A抢占到了锁,他要执行await的方法,那这个时候AQS队列中的当前线程是A,并且线程A也会被放在condition队列中,
  • 那这个时候他就要释放锁,这样好把aqs中的位置空出来,这个时候线程B就可以对锁进行抢占了,那线程B抢占到锁就有机会去调用signal方法去唤醒
    •  线程B为什么知道他可以对锁进行抢占了呢,那是因为在上面j释放锁的时候调用了ava.util.concurrent.locks.AbstractQueuedSynchronizer#release,这个方法调用了unparkSuccessor(),他是对aqs队列中的线程进行唤醒的方法,我们上一篇讲过
//这里不用考虑线程安全性,因为在lock中执行的
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 这里把当前线程加入到队列中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

signal:

  • 要把被阻塞的线程先唤醒
  • 把等待队列中的线程转移到AQS队列中
  • 再次回到await方法中直接抢占锁
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //得到当前的等待队列
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
//唤醒等待队列中的一个线程
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) {
   //如果没有办法修改这个节点的状态,那证明当前节点已经被取消,那就去操作下一个节点,返回到do代码块中,把当前线程抛弃
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

 //上篇说这个是尾插法把节点插入队列,这里就是把等待队列插入aqs队列的尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //进行唤醒,唤醒后因为cpu记住了上次阻塞的地方,所以从【await()->LockSupport.park(this)】这里进行唤醒
        LockSupport.unpark(node.thread);
    return true;
}

再次回到await方法中直接抢占锁(现在就执行这里的代码,acquireQueued()这个是aqs中的逻辑,实现锁的抢占,按照aqs的那套执行,上篇已经分析过)

public final void await() throws InterruptedException {
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

Condition被使用在哪里呢?

实际上我们直接使用Condition的地方很少,主要用在阻塞队列中(阻塞队列一般被使用在线程池中),生产者/消费者流量缓冲等等,我们这里主要讲讲什么是阻塞队列。

【阻塞队列】:阻塞队列是一种线性表,允许一端插入另外一端删除(FIFO先进先出),可以使用【数组】和】进【链表】进行实现。当你要插入数据的时候支持阻塞插入,当取数据出来的时候支持阻塞移除,换言之,当你插入的时候你的队列满了,那插入数据的线程就要等待(阻塞),如果队列空了,则阻塞移除元素的线程。实际上就是一个生产者消费者模型。一般来说,有两种队列,一种【有界队列】、和【无界队列】(实际上也是有大小的,只不过他可以支持很大,直到你的内存爆炸)

 

 基于condition实现一个阻塞队列(这里的实现有点想juc中的java.util.concurrent.ArrayBlockingQueue,只不过人家使用数组来实现的阻塞队列)

public class ConditionQueue {
    // 容器
    private List<String> items;
    // 表示已经添加的元素个数
    private volatile int size;
    //容器的容量
    private volatile int count;

    private Lock lock = new ReentrantLock();
    //take
    private Condition notEmpty = lock.newCondition();
    //add
    private Condition notFull = lock.newCondition();

    private ConditionQueue(int count) {
        this.count = count;
        items = new ArrayList<>();
    }

    private void put(String item) throws InterruptedException {
        lock.lock();
        try {
            if (size >= count) {
                // 队列满了,需要等待
                System.out.println("full of pipeline");
                //这里就是阻塞当前线程,然后把当前线程阻塞在队列中,当阻塞的时候就通知了aqs中的头结点的下一个线程对锁进行抢占,这个时候执行take的线程就抢占到了锁
                notFull.await();
            }
            ++size;
            items.add(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    private String take() throws InterruptedException {
        lock.lock();
        try {
            if (size == 0) {
                System.out.println("empty of pipeline");
                notEmpty.await();
            }
            --size;
            String item = items.remove(0);
            // 同理,他消费了其中的一个数据,然后他就去把上面的阻塞队列中的执行put方法的线程唤醒(把阻塞队列中的线程放到aqs队列中)然后aqs就会进行他的逻辑进行锁的抢占
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionQueue conditionQueue = new ConditionQueue(10);
        Thread thread = new Thread(() -> {
            Random random = new Random();
            for (int i = 0; i < 1000; i++) {
                String item = "item" + i;
                try {
                    conditionQueue.put(item);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(random.nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        //这里是让生产数据的线程先去生产一下数据,
        Thread.sleep(100);
        Thread thread1 = new Thread(() -> {
            Random random = new Random();
            //进行自旋
            for (; ; ) {
                try {
                    System.out.println(conditionQueue.take() + "被消费了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(random.nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
    }
}

阻塞队列中常用的方法

添加元素

  • add():如果队列满了就抛出异常
  • offer():如果队列满了就返回false
  • put():如果队列满了就一直阻塞
  • offer(timeout):如果队列满了就先阻塞你传入的timeout的时间,否则就返回false

移除元素的时候

  • element:元素为空抛异常
  • peak:true / false
  • take:队列一直阻塞
  • poll(timeout):如果超时还没有产生数据,那就返回null

JUC中的阻塞队列

JUC中针对不同的场景,有不同的阻塞队列为我们提供

ArrayBlockingQueue:基于数组

LinkedBlockingQueue:基于链表(单向链表)

PriorityBlockingQueue:基于优先级队列,可以看到他是基于Comparator进行实现的,所以在使用它的时候需要去实现这个接口。

DelayQueue:你可以设置你的元素多久执行,这里有点像rabbitmq中的延迟队列,比如说你的订单15分钟没有支付就取消,那就可以使用这个来做

@ToString
public class DelayQueueExample implements Delayed {
    String orderId;
    long start=System.currentTimeMillis();
    long time;

    public DelayQueueExample(String orderId, long time) {
        this.orderId = orderId;
        this.time = time;
    }

    public static void main(String[] args) {

    }

    //这里表示下次执行的时间
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert((start+time)-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    // 这里对任务进行排序(根据时间的先后做比较)
    @Override
    public int compareTo(Delayed o) {
        return (int) ((int)this.getDelay(TimeUnit.MICROSECONDS)-o.getDelay(TimeUnit.MICROSECONDS));
    }
}

测试

public class DelayQueueTest {
    static DelayQueue delayQueue = new DelayQueue();
    public static void main(String[] args) throws InterruptedException {
        delayQueue.offer(new DelayQueueExample("1001", 1000));
        delayQueue.offer(new DelayQueueExample("1002", 5000));
        delayQueue.offer(new DelayQueueExample("1003", 4000));
        delayQueue.offer(new DelayQueueExample("1004", 7000));
        delayQueue.offer(new DelayQueueExample("1005", 8000));
        delayQueue.offer(new DelayQueueExample("1007", 3000));
        delayQueue.offer(new DelayQueueExample("1008", 2000));
        while (true){
            Delayed take = delayQueue.take();
            System.out.println(take);
        }
    }

}

这里就是按照你的指定的时间顺序依次执行的

SynchronousQueue:没有任何存储结构(因为没有存储容器,当生产者去生产数据的时候,没有消费者消费,那就会阻塞,反之亦然,当消费者去消费,而没有生产者的时候消费者也会阻塞。那多个消费者和生产者阻塞的话必然会产生一个阻塞队列,然后互相去唤醒,有点像AQS,实际上利用了阻塞队列来控制了生产者和消费者),线程池中就只用巧妙了使用了这个java.util.concurrent.Executors#newCachedThreadPool()

 

posted @ 2021-06-17 23:41  UpGx  阅读(181)  评论(0编辑  收藏  举报