如何应对Android面试官->阻塞队列和线程池原理,手写自动收货系统核心实现

阻塞队列

队列

一种数据结构,先进先出;

阻塞队列

队列满了,不能放入新的数据,这个放的操作就是阻塞队列;

队列空的,想要从队列中取出数据,这个取的操作就是阻塞队列;

阻塞队列专用接口:BlockingQueue

阻塞队列中不仅仅是有阻塞方法,还有非阻塞方法;

阻塞队列中的接口一般都是成对儿出现的,例如 add/remove offer/poll take/put

add/remove 是非阻塞的,但是往一个满的队列添加数据的时候会抛出一个异常,往一个空的队列删除数据的时候会抛出一个异常;

offer/poll 是非阻塞的,但是往一个满的队列添加数据的时候会返回一个 false,往一个空的队列删除数据的时候会返回一个 null;

take/put 是阻塞的;

阻塞队列常用来解决什么问题?

通常用来解决生产者与消费者模式;生产者将产物交给队列,消费者从队列中获取,无需关心产物来自哪里,解耦生产者和消费者;

一种是生产者速度大于消费者速度,一种是消费者速度大于生产者速度,为了平衡生产者和消费者之间的这种关系,中间添加了一层容器,生产者只管将生产的内容放入容器,消费者只管从容器中获取数据内容,无需关心内容从哪里来的;

常用的阻塞队列

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列;

LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列;

PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列;

DelayQueue:一个使用优先级队列实现的无界阻塞队列;

SynchronousQueue:一个不存储元素的阻塞队列;

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列;

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列;

**有界:**队列长度是有限制的,满了以后生产者就会被阻塞(规定了最大容量);

**无界:**可以不停的往里面放东西,不会被阻塞(没有规定最大容量);

构造方法中指明了最大容量;

构造方法中指明的是初始容量,可以无限的往里面添加内容;

DelayQueue:支持元素的延迟获取,DelayQueue 对泛型进行了约束,它是 Delayed 的子类,而 Delayed 又是 Comparable 的子类;Comparable 用来做比较,getDelay() 返回当前元素当前实测的一个剩余时长,也就是说当我们往一个 DelayQueue 中放入一个元素的时候,比较的是当前元素的一个剩余时长,谁的时间短,谁就排在队列的前面,最先被取出来,同时 DelayQueue 在阻塞的情况下还比 BlockingQueue 多了一个约束,什么约束呢?就是说:当你从 DelayQueue 中取元素的时候,如果元素剩余时间还没到,是获取不到元素的。必须要等到时间到了才能获取到,通常用来设计单机系统;

SynchronousQueue:当我们使用生产者线程往 SynchronousQueue 放入元素的时候,就必须同时一个消费者线程在对端使用 take 方法将这个元素取走,否则生产者是不能继续往里面添加元素的,常用于数据的直接传递;

LinkedTransferQueue:比其他队列多了一个 transfer 方法,尝试传输,当生产者把元素通过队列传给消费者的时候,它发现正好有一个消费者在等待着获取元素的时候,那么它就会把这个元素直接交给消费者,不再往队列里面放,但是这里有一个约束:如果没有消费者来接收这个元素,那么 transfer 就会阻塞在那里,直到有消费者接收这个元素之后,transfer 才会返回;而 tryTransfer 会先进行试探,试探这个元素有没有消费者接收,没有消费者接收,就会把这个元素放入容器里,tryTransfer 马上就会结束;

LinkedBlockingDeque:一般的队列都是一个入口,一个出口,而 LinkedBlockingDeque 队列可以同时作为出口和入口;

线程池

什么是线程池?

为了节省线程的创建和销毁时间,缩短任务总的执行时间,准备一批线程放在那里统一管理分配使用,而存放这批线程的空间,就叫线程池;

为什么需要线程池?

new Thread,创建线程,它会消耗操作系统资源,不管是创建线程还是销毁线程都是有资源消耗的;

执行一个线程需要的时间:T1、创建线程时间;T2、任务执行时间;T3、线程销毁时间;

所以线程是稀缺昂贵的资源;

如何创建线程池,各个参数的含义?

  1. corePoolSize
  • 当前线程池的核心线程数;
  1. int maximumPoolSize
  • 当前线程池的最大线程数,表示当前线程池所能够使用的最大线程数量;
  1. long keepAliveTime
  • 存活时间;用来控制空闲的线程的存活时间,如果超过了存活时间,线程就会被销毁;
  1. TimeUnit unit
  • 存活时间单位 s/ms;
  1. BlockingQueue workQueue
  • 缺省阻塞队列实现;

  • 当任务数量大于线程数量的时候,超过的任务数放到阻塞队列中,例如:maximumPoolSize 设置的 100,但是我们往线程池中提交了 1000 个线程,那么剩余的 900 个就会放入这个阻塞队列中;

  • 尽量配置成有界的阻塞队列,无界的可能会撑爆机器;

  1. ThreadFactory threadFactory
  • 线程创建的时候,可以允许我们对线程做一点点微调整工作,通常给线程定一个名字;
  1. RejectedExecutionHandler handler
  • 拒绝策略,对超出线程池能力的任务提出的一种拒绝策略;

  • 假设最大线程数和阻塞队列一共可以装载 2000 个,但是提交了 2001 个任务,不能处理了,就需要这个拒绝策略来拒绝(怎么处理超出能力之外的任务);

  • 缺省拒绝策略;

  • DiscardOldestPolicy:直接丢弃最老的任务,排在队列最前面的扔掉,执行当前任务;

  • AbortPolicy:直接抛出异常;

  • CallerRunsPolicy:让调用者线程执行任务,哪个线程往线程池提交任务,就由谁来执行(you can you up);

  • DiscardPolicy:把最新提交的任务直接抛弃;

任务提交

  1. execute():无返回结果;
  2. submit():有返回结果,当需要关心返回结果的时候使用此方法;

任务中断

  1. shutdown(): 尝试关闭一个线程池,把所有没有执行任务的线程进行一个中断;
  2. shutdownNow():不管当前线程有没有执行,都尝试进行中断(不一定会成功,线程中断是一种协作机制,完全取决于有没有良好的处理中断信号);

根据任务类型,如何合理配置参数?

  1. CPU密集型任务
  • 频繁从内存中取数据计算的任务就是 CPU 密集型任务;

  • maximumPoolSize 的值不要超过机器的 CPU 核心数;

  • 通过接口 Runtime.getRuntime().availableProcessors() 来获取 CPU 的核心数,顶多加个1(因为机器的内存有限,操作系统会把磁盘一部分划分出来作为虚拟内存,+1 是为了防止页缺失状态);

  • 页缺失状态:操作系统划分为真实内存和磁盘,但是会在磁盘上开辟一块空间作为虚拟内存,线程执行的数据一部分放在真实内存中,一部分可能放在虚拟内存中,如果数据放在虚拟内存中,那么操作系统不得不把这些数据调度到内存中,一旦发生这种情况,那么操作系统就会让线程进入一种叫做页缺失的状态(等到数据被调度到内存之后,再把线程唤醒,让线程继续工作);

  1. IO密集型任务
  • 网络通信,读写磁盘的任务就是 IO 密集型任务;
  • maximumPoolSize 的值一般是 CPU 核心数的 2 倍;
  1. 混合密集型任务
  • 既有 CPU 密集型 又有 IO 密集型就是混合型任务;

  • 如果两个任务耗时差不多的话,就考虑拆分成两个线程池;

  • 如果两个任务耗时相差很大,哪个大设置哪个类型,例如 CPU 耗时 10ms,IO 耗时5s,那么就设置成 IO 密集型;

任务执行顺序(线程池工作机制)

  1. 提交任务,先交给 corePoolSize 中的线程执行;

  2. corePoolSize 满了之后,交给 BlockingQueue 队列;

  3. BlockingQueue 队列满了之后才交给最大线程数 maximumPool 中的线程;

  4. 最大线程数满了之后,交给拒绝策略进行拒绝处理;

整体流程图如下:

针对 IO 密集型任务,这里额外涉及到的两个知识点:DMA 机制、零拷贝技术;

DMA机制(IO操作不使用CPU)

CPU 在操作 IO 设备的时候,它不会自己去读写,而是向磁盘控制器,网络控制器发送一个信号「你要做什么事情,做完之后通知我」磁盘控制器或者网络控制器做完之后就会给 CPU 发出一个中断信号「硬件含义的中断」意思是「我处理完了,请你 CPU 处理」;这个过程 CPU 完全不参与,全部交给磁盘控制器来操作;

零拷贝

现代 OS 提出了两个空间概念:内核空间和用户空间,内核空间共享,用户空间独立,用户空间不能直接访问内核空间,只能通过接口( Binder 机制),例如:用户空间想访问内核空间网络硬件接口,只能通过内核空间提供的接口,因为用户空间不能直接访问网卡,需要将网卡提供的数据交给内核空间,内核空间再把数据拷贝到用户空间,这个交替的过程没有任何修改,就是一个简简单单的数据拷贝。当用户空间修改完数据之后,还得拷贝到内核空间交给网卡,那么这么一次网络通信发生了至少两次的拷贝,毫无意义,所以提出了零拷贝技术「OS 允许用户空间在内核空间单独申请一个空间,并且允许用户空间可以直接访问这块空间,那么所有的交互都直接通过这块空间进行,无需拷贝」;

线程池中的线程如何被复用?

线程执行了 run 方法会终止,抛出了异常会终止,那么就让当前线程的 run 方法执行不完,让当前线程在阻塞队列上 take,被阻塞;也就是当前线程调用阻塞队列的 take 方法使其阻塞起来,有任务 put 进来了再继续执行;

订单自动收货系统核心实现(队列 + 线程池)

我们在某宝购物之后,当订单收货之后,如果一段时间没有点击收货,那么就会自动收货,核心实现如下:

自动收货系统可以使用 DelayQueue 来实现,通过两个线程,一个是放需要自动收货的线程,一个是取需要自动收货的线程;

订单数据bean

arduino 复制代码
public class OrderItem {    
    private final String orderNum;    
    private final double orderMoney;    
    public OrderItem(String orderNum, double orderMoney) {              
        this.orderNum = orderNum;        
        this.orderMoney = orderMoney;    
    }    

    public String getOrderNum () {        
        return orderNum;    
    }    

    public double getOrderMoney() {        
        return orderMoney;    
    }
}

订单过期和排序

使用 DelayQueue 需要实现 Delayed 接口,用来获取过期时间以及过期时间的排序

kotlin 复制代码
public class Item<T> implements Delayed {    
    // 到期时间    
    private long activeTime;    
    // 订单    
    private  T data;    
    
    public Item(long expireTime, T data) {        
        this.activeTime = expireTime * 1000 + System.currentTimeMillis();        
        this.data = data;    
    }    
    
    public long getActiveTime () {        
        return activeTime;    
    }    
    public T getData() {        
        return data;    
    }    
    
    // 返回到激活日期的剩余时间    
    @Override    
    public long getDelay(TimeUnit unit) {        
        return unit.convert(this.activeTime - System.currentTimeMillis(), unit);    
    }    
    
    @Override    
    public int compareTo(Delayed o) {        
        long d = (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));        
        if (d == 0) {            
            return 0;        
        } else {            
            if (d < 0) {                
                return -1;            
            } else {                
                return 1;            
            }        
        }    
    }
}

存放自动收货订单线程

java 复制代码
public class PutOrderItem implements Runnable{    
    private DelayQueue<Item<OrderItem>> putQueue;    
    public PutOrderItem(DelayQueue<Item<OrderItem>> queue) {        
        this.putQueue = queue;    
    }    
    
    @Override    
    public void run() {        
        OrderItem orderItem = new OrderItem("PDD12345", 400);        
        Item<OrderItem> item = new Item<>(5, orderItem);        
        putQueue.offer(item);        
        System.out.println("订单5秒后超时: " + orderItem.getOrderNum() + "" + orderItem.getOrderMoney());        
        
        OrderItem orderItem1 = new OrderItem("PDD54321", 500);        
        Item<OrderItem> item1 = new Item<>(8, orderItem1);        
        putQueue.offer(item1);        
        System.out.println("订单8秒后超时: " + orderItem1.getOrderNum() + "" + orderItem1.getOrderMoney());    
    }
}

获取自动收货订单线程

kotlin 复制代码
public class FetchOrderItem implements Runnable {    
    private DelayQueue<Item<OrderItem>> fetchQueue;    
    public FetchOrderItem(DelayQueue<Item<OrderItem>> queue) {        
        this.fetchQueue = queue;    
    }    
    @Override    
    public void run() {       
        while (true) {            
            try {                
                Item<OrderItem> item = fetchQueue.take();                
                OrderItem data = item.getData();                
                System.out.println("获取到的需要自动收货的订单:" + data.getOrderNum() + ", " + data.getOrderMoney());            
            } catch (InterruptedException e) {                
                e.printStackTrace();            
            }        
        }    
    }
}

启动线程

ini 复制代码
public class TestDelayQueue {    
    
    public static void main(String[] args) throws InterruptedException {        
        DelayQueue<Item<OrderItem>> queue = new DelayQueue<>(); 
        
        // 使用线程池       
        PutOrderItem putOrderItem = new PutOrderItem(queue);
        //        Thread putThread = new Thread(putOrderItem);
        //        putThread.start();        
        FetchOrderItem fetchOrderItem = new FetchOrderItem(queue);
        //        Thread fetchThread = new Thread(fetchOrderItem);
        //        fetchThread.start();  
        ExecutorService threadPool = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS, 
            new ArrayBlockingQueue<Runnable>(10), new ThreadPoolExecutor.DiscardOldestPolicy())      
        // ExecutorService threadPool = Executors.newFixedThreadPool(2);        
        threadPool.execute(putOrderItem);        
        threadPool.execute(fetchOrderItem);
         
        // 这里用来打印当前进度       
        for (int i = 0; i < 20; i++) {            
            Thread.sleep(500);            
            System.out.println("i*500 = " + i*500);        
        }        }
}

简历润色

简历上可写:深度理解阻塞队列和线程池原理,并熟练运用,可手写自动收货系统核心实现;

下一章预告

带你玩转 AQS 和 volatile

相关推荐
以后不吃煲仔饭10 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师11 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
The_Ticker17 分钟前
CFD平台如何接入实时行情源
java·大数据·数据库·人工智能·算法·区块链·软件工程
大数据编程之光39 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
诸神黄昏EX43 分钟前
Android 分区相关介绍
android
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
ExiFengs1 小时前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj1234567891 小时前
JDK1.8新增特性
java·开发语言
繁依Fanyi1 小时前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:打开 PDF 时自动显示书签或缩略图
java·pdf·.net