JUC多线程与高并发面试题——阻塞队列

一、队列+阻塞队列

    阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下所示:

    JUC多线程与高并发面试题——阻塞队列_第1张图片

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
    当阻塞队列是满时,往队列里添加元素的操作将会被阻塞
  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素;
    同样,试图往已满的阻塞队列中添加元素的线程也会被阻塞,直到其他线程从阻塞队列中移除一个或多个元素或者完全清空队列后,使队列重新变得空闲起来并后续新增。

二、为什么用阻塞队列?有什么好处?

    在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被唤醒。

    为什么需要BlockingQueue?

    好处是:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都一手包办了。

    在concurrent包发布之前,在多线程环境下,我们每个程序员都必须自己去控制这些线程,尤其还要兼顾效率和线程安全,而这会给我们程序带来不小的复杂度。

三、BlockingQueue的核心方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
抛出异常 当阻塞队列满时,再往队列里add插入元素会抛出IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛出NoSuchElementException
特殊值 插入方法,成功true,失败false
移除方法,成功返回队列移除的元素,队列里没有就返回null
一直阻塞 当阻塞队列满时,生产者线程继续往队列里put元素,队列一直阻塞生产线程直到put数据or响应中断退出。
当阻塞队列空时,消费线程试图从队列里take元素,队列会一直阻塞消费线程直到队列可用。
超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产线程会退出。

四、架构梳理+种类分析

4.1 架构梳理

JUC多线程与高并发面试题——阻塞队列_第2张图片

4.2 种类分析

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但是大小默认为Integer.MAX_VALUE)阻塞队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDueue:由链表结构组成的双向阻塞队列

4.3 SynchronousQueue

4.3.1 理论

    SynchronousQueue没有容量。

    与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。

    每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

4.3.2 SynchronousQueueDemo 

package com.yuxx.juc;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        final BlockingQueue blockingQueue = new SynchronousQueue();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName() + "\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "\t" +blockingQueue.take());
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "\t" +blockingQueue.take());
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "\t" +blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

 五、阻塞队列用在哪里

  • 生产者消费者模式
  • 线程池
  • 消息中间件

5.1 生产者消费者模式

5.1.1 sync_wait版

package com.yuxx.juc.pac;
 
/*
 * 生产者和消费者案例
 * main方法执行完毕后,程序并没有执行结束。
 * 模拟流程:
 *   前提条件:生产者加了延迟,会使得消费者先获取锁
 *  初始条件:product:0;生产者剩余循环次数:2;消费者剩余循环次数:1;
 *  流程:
 *   1.消费者线程先进入sale方法,此时product=1,循环次数=1;
 *   2.product<=0,进入if代码块,提示“缺货!”;执行wait()方法后,线程进入等待状态,释放锁的资源;
 *   4.生产者线程进入add方法,此时product=1,循环次数=2;
 *   4.product<1,进入else代码块,product=product+1=1,将其他线程全部唤醒,生产者线程本次循环结束,生产者循环次数-1=1;
 *   5.此时消费者线程之前的循环并没有结束,而生产者线程的循环次数还有一次,二者开始抢锁。
 *   6.假设锁被消费者线程抢到,之后线程继续从sale方法的wait()之后开始执行。
 *   7.sale方法wait()方法后没有可执行代码,跳出循环,消费者线程本次循环结束,消费者线程剩余循环次数-1=0,锁资源被释放。
 *   8.生产者线程还有一次循环次数,生产者线程进入add()方法,此时product=1,循环次数=1。
 *   9.product>=1,进入if代码块,提示“产品已满!”;执行wait()方法,线程进入等待状态,释放锁的资源。
 *   10.此时生产者线程被挂起,消费者线程循环次数为0,没有机会再执行消费者线程。生产者线程无法被唤醒。故程序最终处于线程等待状态,无法被唤醒,程序无法结束。
 * 解决办法:
 *   方法1:去掉add()和sale()方法里的else代码块,将else代码里的代码挪到原来的if代码块之下。
 *     此解决方案不适用于多个消费者和多个生产者的情况。
 *   方法2:进货和卖货的if判断改为where
 */
public class TestProductorAndConsumer4 {
 
  public static void main(String[] args) {
     Clerk4 clerk = new Clerk4();
     Productor4 productor = new Productor4(clerk);
     Consumer4 consumer = new Consumer4(clerk);
     new Thread(productor,"生产者 A").start();
     new Thread(consumer,"消费者 B").start();
     new Thread(productor,"生产者 C").start();
     new Thread(consumer,"消费者 D").start();
  }
}
 
//生产者
class Productor4 implements Runnable{
  private Clerk4 clerk;
 
  public Productor4(Clerk4 clerk) {
     this.clerk = clerk;
  }
 
  @Override
  public void run() {
     for (int i = 0; i < 40; i++) {
       try {
          Thread.sleep(400);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
       clerk.add();
     }
  }
}
 
//消费者
class Consumer4 implements Runnable{
  private Clerk4 clerk;
 
  public Consumer4(Clerk4 clerk) {
     this.clerk = clerk;
  }
 
  @Override
  public void run() {
     for (int i = 0; i < 40; i++) {
       clerk.sale();
     }
  }
}
 
//店员
class Clerk4{
  //产品数量
  private int product = 0;
 
  //进货
  public synchronized void add() {//product:1,循环次数:1
     while(product >= 1) {//为了避免虚假唤醒问题,wait()应该总是使用在循环中。
       System.out.println("产品已满!");
       try {
          this.wait();
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
     System.out.println(Thread.currentThread().getName() + ":" + ++product);
 
     this.notifyAll();
  }
 
  //卖货
  public synchronized void sale() {//product:1;循环次数:0
     while(product <= 0) {
       System.out.println("缺货!");
       try {
          this.wait();
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
     System.out.println(Thread.currentThread().getName() + ":" + --product);
 
     this.notifyAll();
  }
}

5.1.2 lock_condition版

package com.yuxx.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 题目:synchronized和Lock有什么区别?用新的Lock有什么好处?你举例说说?
 * 1.原始构成
 *    synchronized是关键字,属于JVM层面,
 *      monitorenter(底层通过monitor对象来完成,其实是wait/notify等方法依赖于monitor对象,只有在同步块或方法中才能调wait/notify等方法)
 *      monitorexit
 *    Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
 * 2.使用方法
 *    synchronized不需要用户去手动释放锁,当synchronized代码执行完毕后系统会自动让线程释放对锁的占用
 *    ReentrantLock则需要用户去手动释放锁,若没有主动释放锁,就由可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成。
 * 3.等待是否可中断
 *    synchronized不可中断,除非抛出异常或正常运行结束。
 *    ReentrantLock可中断:
 *      > 设置超时方法tryLock(long timeout,TimeUnit unit)
 *      > lockInterruptibly()放代码块中,调用interrupt()方法可中断
 * 4.加锁是否公平
 *    synchronized非公平锁
 *    ReentrantLock两者都可以,默认非公平锁,构造方法可传入boolean值,true为公平锁,false为非公平锁。
 * 5.锁绑定多个条件Condition
 *    synchronized没有
 *    ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么唤醒全部线程。
 *
 * ====================================================================================================================
 *
 * 题目:多线程之间按顺序调用,实现A-B-C三个线程启动,要求如下:
 * AA打印5次,BB打印10次,CC打印15次
 * 紧接着
 * AA打印5次,BB打印10次,CC打印15次
 * ...
 * 来10轮
 */
class ShareResource{
    private int number = 1;//A:1  B:2  c:3
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5(){
        lock.lock();
        try{
            //1 判断
            while (number != 1){
                c1.await();
            }
            //2 干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3 通知
            number = 2;
            c2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void print10(){
        lock.lock();
        try{
            //1 判断
            while (number != 2){
                c2.await();
            }
            //2 干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3 通知
            number = 3;
            c3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void print15(){
        lock.lock();
        try{
            //1 判断
            while (number != 3){
                c3.await();
            }
            //2 干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3 通知
            number = 1;
            c1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                shareResource.print5();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                shareResource.print10();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 1; i <=15 ; i++) {
                shareResource.print15();
            }
        },"C").start();
    }
}

5.1.3 阻塞队列版

package com.yuxx.juc;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyResource{
    private volatile boolean FLAG = true;//默认开启,进行生产+消费
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue blockingQueue = null;

    public MyResource(BlockingQueue blockingQueue){
        this.blockingQueue = blockingQueue;
    }

    public void product() throws Exception{
        String data = null;
        boolean retValue = false;
        while (FLAG){
            data = atomicInteger.incrementAndGet() + "";
            retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if(retValue){
                System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "成功");
            }else{
                System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t大老板叫停,表示FLAG=false,生产动作结束。");
    }

    public void consume() throws Exception{
        String result = null;
        while (FLAG){
            result = blockingQueue.poll(2L,TimeUnit.SECONDS);
            if(null == result || result.equals("")){
                FLAG = false;
                System.out.println(Thread.currentThread().getName() + "\t超过2秒钟没有取到蛋糕,消费退出");
                System.out.println();
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t消费队列蛋糕" + result + "成功");
        }
    }

    public void stop() throws Exception{
        this.FLAG = false;
    }
}

/**
 * volatile/CAS/atomicInteger/BlockingQueue/线程交互
 */
public class ProdConsumer_BlockingQueueDemo {

    public static void main(String[] args) throws Exception {
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t生产线程启动");
            try {
                myResource.product();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Prod").start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
            System.out.println();
            System.out.println();
            try {
                myResource.consume();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Consume").start();
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("5秒钟时间到,大老板main线程叫停,活动结束");
        myResource.stop();
    }
}

打印结果:

JUC多线程与高并发面试题——阻塞队列_第3张图片

转载于:https://my.oschina.net/alexjava/blog/3102841

你可能感兴趣的:(JUC多线程与高并发面试题——阻塞队列)