Java多线程 一篇搞懂线程池

Java多线程 线程池的使用

1、线程池的简介

1.1 什么是线程池

线程池,本质上是一种对象池,用于管理线程资源。在任务执行前,需要从线程池中拿出线程来执行。在任务执行完成之后,需要把线程放回线程池
通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。

1.2 线程池的好处

(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统的响应速度,当有任务到达时,通过复用已经存在的线程,无需创建新的线程便可立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM (内存溢出),并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。

2、实现原理

2.1 线程池处理流程图

在这里插入图片描述
大致流程可以概括为:
(1)首先判断核心线程池是否已满,如果不是,则创建线程执行任务
(2)如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中
(3)如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务
(4)如果线程池也满了,则按照拒绝策略对任务进行处理

2.2 ThreadPoolExecutor的处理流程

在这里插入图片描述
疑难:
1、线程池为什么需要使用(阻塞)队列?

(1)因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
(2)线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲

2、线程池为什么要使用阻塞队列而不使用非阻塞队列?

(1)阻塞队列可以保证任务队列中没有任务时(也就是队列为空时)阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
(2)当队列中有任务时才唤醒对应线程从队列中取出消息进行执行

3、常见线程池的创建

3.1 Executors简单创建4种线程池

(1)SingleThreadPool()创建单一线程线程池

这个线程池只有一个线程若多个任务被提交到此线程池,那么会被缓存到队列(队列长度为Integer.MAX_VALUE)。当线程空闲的时候,按照FIFO的方式进行处理。

图解:
在这里插入图片描述
实例:

package ThreadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SignalThreadPoolTest {
    public static void main(String args[]){
        //创建单线程的线程池
        ExecutorService singal = Executors.newSingleThreadExecutor();

        for(int i = 0; i < 5; ++i){
            int task = i;
            singal.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程:" + Thread.currentThread().getName() + "正在执行" + task);
                }
            });
        }
        System.out.println("五个任务开始执行!");
    }
}

运行结果:

五个任务开始执行!
线程:pool-1-thread-1正在执行0
线程:pool-1-thread-1正在执行1
线程:pool-1-thread-1正在执行2
线程:pool-1-thread-1正在执行3
线程:pool-1-thread-1正在执行4

从结果可以看出,程序始终只创建了一个线程,而且线程的执行是按FIFO顺序执行的。

(2)FixedThreadPool()创建固定数量的线程池

和创建单一线程的线程池类似,只是这儿可以并行处理任务的线程数更多一些罢了。若多个任务被提交到此线程池,会有下面的处理过程。

图解:
在这里插入图片描述
实例:

package ThreadPool;

        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

public class SignalThreadPoolTest {
    public static void main(String args[]){
        //创建单线程的线程池
        ExecutorService threadExecutor = Executors.newFixedThreadPool(2);

        for(int i = 0; i < 5; ++i){
            int task = i;
            threadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程:" + Thread.currentThread().getName() + "---------正在执行" + task);
                }
            });
        }
        System.out.println("五个任务开始执行!");
    }
}

运行结果:

五个任务开始执行!
线程:pool-1-thread-1---------正在执行0
线程:pool-1-thread-2---------正在执行1
线程:pool-1-thread-1---------正在执行2
线程:pool-1-thread-2---------正在执行3
线程:pool-1-thread-1---------正在执行4

(3)CachedThreadPool 创建带缓存的线程池

其核心线程池的长度为0,线程池最大长度为Integer.MAX_VALUE。由于本身使用SynchronousQueue作为等待队列的缘故,导致往队列里面每插入一个元素,必须等待另一个线程从这个队列删除一个元素。

图解:
在这里插入图片描述
该类型的线程池经常用于web服务器处理大量的请求。

(4)ScheduledThreadPool 创建定时调度的线程池

该线程池可以实现定时调度的功能

1、scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),定时调度,每个调度任务会至少等待period的时间,如果任务执行的时间超过period,则等待的时间为任务执行的时间

2、scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit),定时调度,第二个任务执行的时间 = 第一个任务执行时间 + delay

3、schedule(Runnable command, long delay, TimeUnit unit),定时调度,延迟delay后执行,且只执行一次

实例:

package ThreadPool;

        import java.util.concurrent.Executors;
        import java.util.concurrent.ScheduledExecutorService;
        import java.util.concurrent.TimeUnit;

public class SignalThreadPoolTest {
    public static void main(String args[]){
        //创建单线程的线程池
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

        // 定时调度,每个调度任务会至少等待`period`的时间,
        // 如果任务执行的时间超过`period`,则等待的时间为任务执行的时间
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                    System.out.println("11111---"  + System.currentTimeMillis()/1000);
                    System.out.println("-----------1111--------------");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0, 2, TimeUnit.SECONDS);

        // 定时调度,第二个任务执行的时间 = 第一个任务执行时间 + `delay`
        executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    System.out.println("2222---"  + System.currentTimeMillis()/1000);
                    System.out.println("-------------2222-------------");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);

        // 定时调度,延迟`delay`后执行,且只执行一次
        executor.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("5 秒之后执行 schedule");
            }
        }, 5, TimeUnit.SECONDS);
    }
}

结果:

-------------2222-------------
5 秒之后执行 schedule
11111---1583070844
-----------1111--------------
2222---1583070846
-------------2222-------------
2222---1583070853
-------------2222-------------
11111---1583070854
-----------1111--------------
----
----

从结果大致可以看出:
2号线程:满足 第二个任务执行的时间 = 第一个任务执行时间 + delay
1号线程:满足:如果任务执行的时间超过period,则等待的时间为任务执行的时间

3.2 ThreadPoolExecutor手动创建线程池

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);

参数解析:
(1)corePoolSize:线程池中的核心线程数;
(2)maximumPoolSize:线程池中的最大线程数;
(3)keepAliveTime:空闲时间,当线程池数量超过核心线程数时,多余的空闲线程存活的时间,即:这些线程多久被销毁。
(4)unit:空闲时间的单位,可以是毫秒、秒、分钟、小时和天,等等
(5)workQueue:等待队列,线程池中的线程数超过核心线程数时,任务将放在等待队列,它是一个BlockingQueue类型的对象
(6)threadFactory:线程工厂,我们可以使用它来创建一个线程
(7)handler:拒绝策略,当线程池和等待队列都满了之后,需要通过该对象的回调函数进行回调处理

重点关心workQueue,threadFactory,handle这几个参数

1、等待队列-workQueue
等待队列是BlockingQueue类型的,理论上只要是它的子类,我们都可以用来作为等待队列。
(1)ArrayBlockingQueue,队列是有界的,基于数组实现的阻塞队列
(2)LinkedBlockingQueue,队列可以有界,也可以无界。基于链表实现的阻塞队列
(3)SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态。该队列也是Executors.newCachedThreadPool()的默认队列
(4)PriorityBlockingQueue,带优先级的无界阻塞队列

2、线程工厂-threadFactory

Executors的实现使用了默认的线程工厂-DefaultThreadFactory。它的实现主要用于创建一个线程,线程的名字为pool-{poolNum}-thread-{threadNum}。

3、拒绝策略-handler

(1)CallerRunsPolicy // 在调用者线程执行
(2)AbortPolicy // 直接抛出RejectedExecutionException异常
(3)DiscardPolicy // 任务直接丢弃,不做任何处理
(4)DiscardOldestPolicy // 丢弃队列里最旧的那个任务,再尝试执行当前任务

4、 提交任务的几种方式

1、execute(),执行一个任务,没有返回值。

2、submit(),提交一个线程任务,有返回值。
(1)submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。

(2)submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
(3)submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

5、线程池的关闭

(1)shutdown()会将线程池状态置为SHUTDOWN,不再接受新的任务,同时会等待线程池中已有的任务执行完成再结束。

(2)shutdownNow()会将线程池状态置为SHUTDOWN,对所有线程执行interrupt()操作,清空队列,并将队列中的任务返回回来。

另外,关闭线程池涉及到两个返回boolean的方法,isShutdown()和isTerminated,分别表示是否关闭和是否终止。

6、如何配置线程池

(1)CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

(2)IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

(3)混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。


参考博客链接:
https://www.jianshu.com/p/7ab4ae9443b9

发布了30 篇原创文章 · 获赞 19 · 访问量 3894

猜你喜欢

转载自blog.csdn.net/qqq3117004957/article/details/104597767