Java 同步工具与组合类的线程安全性分析( 四 )

  • PriorityBlockingQueue :优先级队列,当我们希望以一定次序处理任务时,它要比 FIFO 队列更实用 。
  • SynchronousQueue :译为同步阻塞队列 。这个队列事实上没有缓存空间,而是维护一组可用的线程 。当队列收到消息时,它可以立刻分配一个线程去处理 。但是如果没有多余的工作线程,那么调用 put 或者 take 会立刻陷入阻塞状态 。因此,仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列 。
  • 下方的代码块是由 SynchronousQueue 实现的简易 Demo,每个线程会抢占式消费消息 。
    var chan = new SynchronousQueue<Integer>();var worker = new Thread(()->{while(true){try {final var x = chan.take();System.out.println("t1 consume: " + x);} catch (InterruptedException e) {e.printStackTrace();}}});var worker2 = new Thread(()->{while(true){try {final var x = chan.take();System.out.println("t2 consume: " + x);} catch (InterruptedException e) {e.printStackTrace();}}});worker.start();worker2.start();for(var i = 0 ; i < 10; i ++) chan.put(i);复制代码基于所有权的角度去分析,生产者 — 消费者模式和阻塞队列一起促进了 串行的线程封闭  。线程封闭对象只能由单个对象拥有,但可以通过在执行的最后发布该对象 ( 即表示之后不会再使用它 ),以表示 "转让" 所有权 。
    阻塞队列简化了转移的逻辑 。除此之外,还可以通过 ConcurrentMap 的原子方法 remove,或者是 AtomicReference 的 compareAndSet ( 即 CAS 机制 ) 实现安全的串行线程封闭 。
    双端队列和工作窃取Java 6 之后增加了新的容器类型 —— Deque 和 BlockDeque,它们是对 Queue 以及 BlockingQueue 的拓展 。Deque 实现了再队列头和队列尾的高效插入和移除,具体实现包括了 ArrayDeque 和 LinkedBlockingDeque 。
    双端队列适用于另一种工作模式 —— 工作窃取 ( Work Stealing ) 。比如,一个工作线程已经完成清空了自己的任务队列,它就可以从其它忙碌的工作线程的任务队列的尾部获取队列 。这种模式要比生产者 —— 消费者具备更高的可伸缩性,因为工作线程不会在单个共享的任务队列上发生竞争 。
    工作窃取特别适合递归的并发问题,即执行一个任务时会产生更多的工作,比如:Web 爬虫,GC 垃圾回收时的图搜索算法 。
    阻塞和中断方法线程可能会被阻塞,或者是暂停执行,原因有多种:等待 I/O 结束,等待获得锁,等待从 Thread.sleep 中唤醒,等待另一个线程的计算结果 。被阻塞的线程必须要在这些 "外因" 被解决之后才有机会继续执行,即恢复到 RUNNABLE ( 也称就绪 ) 状态,等待被再次调度 CPU 执行 。
    这段描述其实对应了 JVM 线程的两个状态:BLOCKING 和 WAITING 。
    1. BLOCKING,当线程准备进入一段新的同步代码块时,因不能获得锁而等待 。
    2. WAITING,当线程已经进入同步代码块之后,在执行的过程中因不满足某些条件而暂停 。这时可以调用 waiting 方法 释放已占据的锁  。其它工作线程得以抢占此锁并执行,直到满足先验条件为真时,其它线程可以通过 notifyAll 方法重新令监视此锁的所有 WAITING 线程再次争锁并继续工作 。 wait / notify / notifyAll 构成了线程之间的协商机制,见下面的代码块 。
    static class Status{public boolean v;}public static void main(String[] args) throws InterruptedException{var status = new Status();status.v = false;var mutex = new Object();new Thread(()->{synchronized (mutex){System.out.println("get mutex");// 此时检测的状态为 false, 进入 WAITING 状态 。if(!status.v) try {mutex.wait();} catch (InterruptedException e) {e.printStackTrace();}// 被唤醒后重新检测状态为 true 。System.out.println(status.v);}}).start();new Thread(()->{synchronized (mutex){// 将状态设置为 true,唤醒上面的线程status.v = true;mutex.notify();}}).start();}复制代码只有处于 RUNNABLE 状态的线程才会实际获得 CPU 使用权 。
    Java中哪些操作会使线程释放锁资源_后端码匠的博客-CSDN博客_线程释放锁资源
    JVM中的线程状态 - 知乎 (zhihu.com)
    在 Java 中,一切会发生阻塞的方法都会被要求处理 InterruptedException 受检异常 。调用阻塞方法的方法也会变成阻塞方法 。线程内部有一个 boolean 类型的状态位表示中断,调用 interrupt 方法可以将该状态位标识为 true  。但是这不意味着该线程就会立刻中断:


    推荐阅读