ThreadPool实现机制

发布时间 2023-03-22 19:15:23作者: 懒懒初阳

Android中阻塞队列的应用有哪些

阻塞队列在 Android 中有很多应用,比如:

  1. 线程池:线程池任务的执行就是基于一个阻塞队列,如果线程池任务已满,则任务需要等待阻塞队列中的其他任务完成。
  2. Handler 消息队列:Handler 的消息队列也是一种阻塞队列。handler发送消息时,首先将消息加入到消息队列中,在空闲状态下 MessageQueue 队列是阻塞的状态,直到队列不为空,Looper 开始轮询 MessageQueue 里面的 Message。

阻塞队列支持两个核心方法,分别为:

  • put(E e):将元素插入队尾,支持阻塞式插入,在容量无限制的情况下一直等待直到队列有空闲位置。
  • take(): 获取且移除此队列的头部,在队列为空时,阻塞等待这个头部可用,并返回被取出的元素。

在 Java 中,阻塞队列的实现类有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue 等等。这些队列通过使用 Lock 和 Condition 来保证队列的线程安全。

阻塞队列的原理

是当阻塞队列中没有数据的时候,读线程将被阻塞;当阻塞队列已满的时候,写线程将被阻塞。阻塞队列通过内部锁来控制读和写操作的相互访问以及有序性,保证在多线程并发的时候,执行插入或读取操作是广泛可接受的,而不会引起任何问题。

线程池是维护一个工作线程的线程容器,用于优化线程的创建和销毁带来的开销。下面简单介绍一下线程池的底层实现原理。

线程池执行流程

  1. 在初始化时,线程池会通过ThreadPoolExecutor构造函数赋值并创建核心线程池;
  2. execute()方法将任务存放到阻塞队列中,如果有空闲线程,则取出最先进入队列的任务开始执行,否则加入等待中的队列中;
  3. 当队列长度达到阈值时,不再继续增加,此时若仍有新任务提交,则以上空闲线程处理任务;若添加的线程数仍超过最大线程数时,则拒绝该任务。

参数说明:

以下是线程池最基本的四类参数。

  • 核心线程数(corePoolSize):线程池中的基本线程数。当任务提交后,分类讨论:

    • 若线程池中的线程数小于 corePoolSize,那么即使线程池中还有空余的线程,也会直接添加一个新线程去处理当前的任务;
    • 若线程池已经有了 corePoolSize 个线程,那么任务就被加入阻塞队列进行缓冲,等待有空闲的线程来处理;
    • 一次性只能创建 corePoolSize 个线程。
  • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当缓冲队列满时,对于大于 corePoolSize 的任务将会此项启动更多的 Thread 来处理用户请求,针对有界和无界不同表现。如有等待队列功能满了后会执行RejectedExecutionException。在我看来这个配置很鸡肋,往往在它小于corepoolsize时候有表现,大于情况视大于多少对线程池复用机制上友好而已。有不对的欢迎评论区讨论下;

  • 队列容量(workQueue):等待任务的队列容量。在调用 execute() 方法时,阻塞队列可以用以下三种类型之一:

    • FIFO(先进先出)队列,使用java.util.concurrent.LinkedBlockingQueue,默认无界限大小。因此 newFixedThreadPool() 和 newSingleThreadExecutor() 的线程数最多也可能会达到 Integer.MAX_VALUE,会导致OOM等问题。但你可指定其大小。
    • LIFO(后进先出)队列,使用java.util.concurrent.LinkedBlockingDeque,特点是新任务插到队尾上,但在 JDK7 中不再使用这个队列,因为它是不符合 Java 应用程序的行为模式的;
    • 优先级队列,使用java.util.concurrent.PriorityBlockingQueue 类实现,按照元素等级来排序,低等级对象会先被获取,对于高等级的任务可以提前执行。
  • 空闲线程销毁时间(keepAliveTime):线程池中没有任务执行时,即空闲状态下的线程没事可做,这些空闲的线程会自动的销毁,首先由coreThreadHandle(正常获得锁的线程)完成剩下工作,然后 wait coreHandle 释放锁资源,休眠到内部队列不为空或超时返回,如果休眠超时则表示该线程超时了,在低于 corePoolSize 的情况下 除非设置allowCoreThreadTimout=true,否则永远不会发生;

实例代码

public class TestThreadPoll implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行...");
    }
}

  public static void main(String[] args) {
      // 创建一个基本的线程池
      ExecutorService executor = Executors.newCachedThreadPool();

      for (int i = 0; i < 10; i++) {
          Runnable task = new TestThreadPoll();
          executor.execute(task);
      }
      executor.shutdown();
  }

上面的创建了一个基本的线程池,并进行了线程数的添加。

线程池排队机制

当线程池中所有的线程都在执行任务时,如果新提交的任务需要被执行,那么这个新的任务会进入到一个阻塞队列中排队等待。而ThreadPoolExecutor中有四种常见的阻塞队列:SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue。

其中SynchronousQueue是没有任何储存功能的阻塞队列,它只负责一个任务的传递,即前一个任务将后一个任务提交给线程池后直接结束,而不是进入等待队列。由于该队列没有储存空间,所以当一个线程提交任务时,该队列会寻找与之对应的另一个线程来接收这个任务。如果此时线程池处于饱和状态,这个任务就有可能被丢弃。

LinkedBlockingQueue则是一个带储存功能的无界队列,也就是说,这个队列可以一直往里添加新的元素。因此,它的长度限制仅仅是由内存大小来控制,如果你的阻塞队列需要存储非常多的任务,那么最好选用这个类型的阻塞队列。

ArrayBlockingQueue也是一个带储存功能的有界队列,它的长度是预设好的,如果你试图往这个队列中添加超过预设长度的任务,那么新增的任务就必须要等待其他任务完成或者被移除后才能加入队列了,换而言之,在这种队列中,无法满足新任务的请求时会出现阻塞情况。

PriorityBlockingQueue则是一个支持优先级排序的无界队列,也就是说,任务可以通过实现Comparable接口来设置一个队列中的优先级关系。当使用优先级队列时,可以为不同的任务设置不同的优先级,程序会按照任务的优先级顺序执行具体的任务。

自实现一个线程池实例

以下是一个简单的自实现线程池示例:

public class MyThreadPool {
    private final BlockingQueue<Runnable> workQueue;
    private final WorkerThread[] workers;

    public MyThreadPool(int nThreads) {
        this.workQueue = new LinkedBlockingQueue<>();
        this.workers = new WorkerThread[nThreads];

        for (int i = 0; i < nThreads; i++) {
            workers[i] = new WorkerThread();
            workers[i].start();
        }
    }

    public void execute(Runnable task) {
        synchronized (workQueue) {
            workQueue.add(task);
            workQueue.notify();
        }
    }

    private class WorkerThread extends Thread {
        @Override
        public void run() {
            while(true) {
                Runnable task;
                synchronized (workQueue) {
                    while (workQueue.isEmpty()) {
                        try {
                            workQueue.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                    task = workQueue.poll();
                }
                try {
                    task.run();
                } catch(Throwable t) {
                    // error handling code
                }
            }
        }
    }
}

这是一个最简单的线程池实现,其核心思想就是:在.WorkerThread类中定义了一个死循环,负责从任务队列中取出任务并执行。如果目前没有任务,则等待新任务到来。

使用时初始化线程池:

MyThreadPool pool = new MyThreadPool(2);

之后将需要做的任务加入线程池中:

pool.execute(new Runnable() {
    @Override
    public void run() {
        // 执行具体的任务代码
    }
});

至此,自实现的线程池实例就完成了。

execute框架解读

execute()是Java中Executor接口的一个方法,它在执行传递给它的Runnable任务时使用。Executor框架提供了一种将任务提交给线程池以异步执行的方式。
当我们想要某段代码在异步环境中运行,又不需要知道每个任务何时完成时(所需时间可能会非常不同)的情况下,就可以使用这个框架。该框架管理和复用线程,以避免耗费创建他们的代价,并且在执行完任务后返回线程到线程池。
execute()方法的目的是在默认池中安排一个任务来执行,由于这个方法只是将任务提交给线程池并立即返回,因此不能保证任务已经执行。这个方法只有在向线程池中添加任务时需要使用,例如对于没有返回值的简单操作或前置条件检查。
以下是execute()的方法语法:

void execute(Runnable command);

参数:

  • command:Runnable对象,该接口定义了需要在线程上执行的任务。

实现:
该方法直接将任务提交给主执行器以异步执行。然后返回而不等待任务的结束。如果需要使用结果,则调用submit方法。

//newFixedThreadPool 被重载用来覆盖默认设置,指定核心线程数和最大线程数相同。
//此示例将线程池大小限制为 5 个工作线程。
ExecutorService executor = Executors.newFixedThreadPool(5);

for (int i = 0; i < 10; i++) {
    Runnable worker = new ExampleRunnable("" + i);
    executor.execute(worker);
}
// 当所有可执行任务都完成后关闭线程池
executor.shutdown();
// 阻塞当前线程直到关闭操作完成
while (!executor.isTerminated()) { }

System.out.println("Finished all threads");

以上代码中,新的线程池有默认的限制,而不需要明确地声明线程池的大小或能力 。

线程池固定大小,当队列中没有可以在工作线程上执行的任务时(即阻止列表为空),则该任务将继续等待直到可用。

在每个可用工作线程上使用while循环反复启动一个Runnable任务并让其运行到池的关闭标志被设置

线程池被关闭之后,isTerminated()方法返回true,并退出while循环。