Thread基础之线程池详解+运用

6 篇文章 0 订阅


在这里插入图片描述

线程池

线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。

为什么要使用线程池?

  • 可重复使用已有线程,避免对象创建、消亡和过度切换的性能开销。
  • 避免创建大量同类线程所导致的资源过度竞争和内存溢出的问题。
  • 支持更多功能。比如,延迟任务线程池(newScheduledThreadPool)和缓存线程池(newCachedThreadPool)等。
  • 创建线程池有两种方式:ThreadPoolExecutor 和 Executors(后续讲解) 。

线程池的好处:

  • 重用存在的线程,减少对象的创建/销毁的开销,性能更佳。

  • 可有效控制最大并发线程数量,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。

  • 提供定时执行、定期执行、单线程、并发数控制等功能。

  • ThreadPoolExecutor 是 J.U.C 中提供的线程池类。

线程池的创建

创建线程池时(new ThreadPoolExecutor())会涉及到如下几个参数(后面有详解版):

  • corePoolSize:线程池的核心线程数量。逻辑上,就是线程池中线程数量的下限。
  • maximumPoolSize:线程池最大线程数。逻辑上,就是线程池中线程数量的上限。
  • poolSize:线程池当前的线程数。它的值会在 corePoolSize 和 maximumPoolSize 之间。
  • workQueue:阻塞队列,存储待执行的任务。很重要,会对线程池运行过程产生重大影响。
  • keepAliveTime:线程没有任务执行时最多保持多久时间终止。
  • unit:keepAliveTime 的时间单位
  • threadFactory:线程工厂,用来创建线程
  • rejectHandler:当拒绝处理任务(例如线程池已满,不再接受新任务)时的策略

不过通常我们并非使用 new 的方式创建线程池。J.U.C 提供了 Executors 工具类,并提供了几个简化线程池创建的方法:

  • Executors.newSingleThreadExecutor()

    创建一个单线程的线程池。该线程池中有且仅有一个工作线程(来处理任务),即,线程池中储于 Running 状态的线程数不能超过 1 。

    当任务数超过 1 时,需要等待。

  • Executors.newFixThreadPool()

    创建一个线程数目固定的线程池。对于添加到任务队列中的任务,如果线程池还有可用线程,那么就执行该任务。如果所有线程已被占用,那么任务的执行将会等到有线程执行完它手头的工作(任务)后才开始。

  • Executors.newScheduledThreadPool()

    同上。还支持定时及周期性任务执行。

  • Executors.newCachedThreadPool()

    创建一个可缓存线程。逻辑上就是 FixThreadPool 的『反面』。

    该线程池一旦发现线程不够用就会创建新线程去执行新添加的任务,并且它会复用已有的线程。线程执行完任务后,如果存活期到期,到期时间内一直未被使用,那么线程池会销毁过期的线程。

线程池的状态

#状态说明
1Running线程池正在运行中
2Shutdown关闭状态之一。这种状态下线程池不再接受新的任务,但是会将已接受的任务处理完。处理完后,线程池会自动进入 Tidying 状态。
3Stop另一种关闭状态。这种状态下不再接受新的任务,并且会放弃已接受的而又未执行的任务。不仅如此,它还会取消正在执行中的任务。取消掉正在执行的任务后,线程池会自动进入 Tidying 状态。
4Tidying这种状态下的线程池意味着不再具有任何功能。其中工作线程数量为 0 。
5Terminated这种状态下线程池彻底停止,接下来就是 JVM 着手回收相关对象。

相关 API

方法说明
execute()提交任务,交给线程池执行。
submit()同上。能够返回执行结果。结合 Callable 和 Future 使用。
shutdown()关闭线程池,等待任务都执行完。即,线程池进入 Shutdown 状态。
shutdownNow()关闭线程池,不等待任务执行完。即,线程池进入 Stop 状态。
getTaskCount()返回线程池已执行和未执行任务总数。即,总共接收的任务数。
getCompletedTaskCount()返回线程中已完成的任务数。
getPoolSize()返回线程池当前的线程数量。
getActiveCount()返回线程池中正在执行任务的线程数量。

ThreadPoolExecutor

1. ThreadPoolExecutor 的使用

线程池使用代码如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    2, 10, 
    10L, TimeUnit.SECONDS, 
    new LinkedBlockingQueue(100));

threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        // 执行线程池
        System.out.println("Hello, Java.");
    }
});

// 以上程序执行结果如下:
// Hello, Java.

ThreadPoolExecutor 构造方法有 4 个,其中参数最多的那个构造方法有 7 个入参,其它 3 个构造方法本质上就是这个"最长构造方法"的简写。

这 7 个参数名称如下所示:

public ThreadPoolExecutor(
        int corePoolSize,   // ①
        int maximumPoolSize,// ② 
        long keepAliveTime, // ③ 
        TimeUnit unit,      // ④
        BlockingQueue<Runnable> workQueue,  // ⑤
        ThreadFactory threadFactory,        // ⑥
        RejectedExecutionHandler handler    // ⑦
    ) {
        // ...
}

其代表的含义如下:

#参数说明
1corePoolSize线程池中的核心线程数,默认情况下核心线程一直存活在线程池中。即,逻辑上,线程池中的最少线程数。
2maximumPoolSize线程池中最大线程数。如果活动的线程达到这个数值以后,ThreadPoolExcecutor 再接收到新任务时,将会阻塞等待,而非创建线程立即执行。
3keepAliveTime线程池中的线程可闲置的最大时长,默认情况下对非核心线程生效。如果闲置时间超过这个时间,非核心线程就会被回收。
如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true ,那么,核心线程也会受该时长影响。
4unit配合 keepAliveTime 使用,keepAliveTime 的值的时间单位。
5workQueue线程池中的任务队列,使用 execute 方法或 submit 方法提交的任务都会存储在此队列中。
6threadFactory为线程池提供创建新线程的线程工厂。
7rejectedExecutionHandler线程池任务队列超过最大值之后的拒绝策略。
RejectedExecutionHandler 是一个接口,里面只有一个 rejectedExecution 方法,可在此方法内添加任务超出最大值的事件处理。
ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:
new ThreadPoolExecutor.DiscardPolicy():丢弃掉该任务,不进行处理
new ThreadPoolExecutor.DiscardOldestPolicy():丢弃队列里最近的一个任务,并执行当前任务
new ThreadPoolExecutor.AbortPolicy():直接抛出 RejectedExecutionException 异常
new ThreadPoolExecutor.CallerRunsPolicy():既不抛弃任务也不抛出异常,直接使用主线程来执行此任务

包含所有参数的 ThreadPoolExecutor 使用代码示例:

public class ThreadPoolExecutorTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                    1, 1,
                    10L, TimeUnit.SECONDS, 
                    new LinkedBlockingQueue<Runnable>(2),
                    new MyThreadFactory(), 
                    new ThreadPoolExecutor.CallerRunsPolicy());

        threadPool.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

class MyThreadFactory implements ThreadFactory {
    private AtomicInteger count = new AtomicInteger(0);
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            String threadName = "MyThread" + count.addAndGet(1);
            t.setName(threadName);
            return t;
    }
}

2. 线程池执行方法

execute 方法和 submit 方法都是用来执行线程池的。它们的区别在于 submit 方法可以接收线程池执行的返回值。

下面分别来看两个方法的具体使用和区别:

// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    2, 10, 
    10L, TimeUnit.SECONDS, 
    new LinkedBlockingQueue(100));

// execute 使用
threadPoolExecutor.execute(new Runnable() {
    @Override   // 逻辑代码
    public void run() {
        System.out.println("Hello, Java.");
    }
});

// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("Hello, 老王.");
        return "Success";
    }
});

System.out.println(future.get());

以上程序执行结果如下:

Hello, Java.
Hello, 老王.
Success

3. 线程池关闭方法

线程池关闭,可以使用 .shutdown.shutdownNow 方法,它们的区别是:

  1. .shutdown 方法:不会立即终止线程池,而是要等所有任务队列中的任务都执行完后才会终止。执行完 shutdown 方法之后,线程池就不会再接受新任务了。

  2. .shutdownNow 方法:执行该方法,线程池的状态立刻变成 STOP 状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

下面用代码来模拟 shutdown() 之后,给线程池添加任务,代码如下:

threadPoolExecutor.execute(() -> {
    for (int i = 0; i < 2; i++) {
        System.out.println("I'm " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }
});
threadPoolExecutor.shutdown();
threadPoolExecutor.execute(() -> {
    System.out.println("I'm Java.");
});

以上程序执行结果如下:

I'm 0

Exception in thread "main" java.util.concurrent.RejectedExecutionException:
Task com.interview.chapter5.Section2`$$Lambda$2`/1828972342@568db2f2 rejected
from java.util.concurrent.ThreadPoolExecutor@378bf509[Shutting down, pool size
= 1, active threads = 1, queued tasks = 0, completed tasks = 0]

I'm 1

可以看出,shutdown() 之后就不会再接受新的任务了,不过之前的任务会被执行完成。

4. 总结

ThreadPoolExecutor 是创建线程池最传统和最推荐使用的方式,创建时要设置线程池的核心线程数和最大线程数还有任务队列集合,如果任务量大于队列的最大长度,线程池会先判断当前线程数量是否已经到达最大线程数,如果没有达到最大线程数就新建线程来执行任务,如果已经达到最大线程数,就会执行拒绝策略(拒绝策略可自行定义)。

线程池可通过 submit() 来调用执行,从而获得线程执行的结果,也可以通过 shutdown() 来终止线程池。

Executors

提示
逻辑上,Executors 是 ThreadPoolExecutor 的工具类。

Executors 可以创建以下 6 种线程池。

#方法说明
1FixedThreadPool创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。
2CachedThreadPool短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。
3SingleThreadExecutor创建一个单线程线程池。
4ScheduledThreadPool创建一个数量固定的线程池,支持执行定时性或周期性任务。
5SingleThreadScheduledExecutor此线程池就是单线程的 newScheduledThreadPool 。
6WorkStealingPoolJava 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

1. FixedThreadPool 使用

[!success] 注意
newFixedThreadPool() 适合执行单位时间内固定的任务

创建固定个数的线程池,具体示例如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
    fixedThreadPool.execute(() -> {
        System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序执行结果如下:

CurrentTime - 2019-06-27 20:58:58
CurrentTime - 2019-06-27 20:58:58
CurrentTime - 2019-06-27 20:58:59

根据执行结果可以看出,newFixedThreadPool(2) 确实是创建了两个线程,在执行了一轮(2 次)之后,停了一秒,有了空闲线程,才执行第三次。

2. CachedThreadPool 使用

根据实际需要自动创建带缓存功能的线程池,具体代码如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

for (int i = 0; i < 10; i++) {
    cachedThreadPool.execute(() -> {
        System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序执行结果如下:

CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46

根据执行结果可以看出,newCachedThreadPool 在短时间内会创建多个线程来处理对应的任务,并试图把它们进行缓存以便重复使用。

3. SingleThreadExecutor 使用

创建单个线程的线程池,具体代码如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

for (int i = 0; i < 3; i++) {
    singleThreadExecutor.execute(() -> {
        System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

以上程序执行结果如下:

CurrentTime - 2019-06-27 21:43:34
CurrentTime - 2019-06-27 21:43:35
CurrentTime - 2019-06-27 21:43:36

4. ScheduledThreadPool 使用

创建一个可以执行周期性任务的线程池,具体代码如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
scheduledThreadPool.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());

以上程序执行结果如下:

CurrentTime:2019-06-27T21:54:21.881
ThreadPool:2019-06-27T21:54:22.845

根据执行结果可以看出,我们设置的 1 秒后执行的任务生效了。

5. SingleThreadScheduledExecutor 使用

创建一个可以执行周期性任务的单线程池,具体代码如下:

ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());

6. WorkStealingPool 使用

Java 8 新增的创建线程池的方式,可根据当前电脑 CPU 处理器数量生成相应个数的线程池,使用代码如下:

ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
    int finalNumber = i;
    workStealingPool.execute(() -> {
        System.out.println("I:" + finalNumber);
    });
}
Thread.sleep(5000);

以上程序执行结果如下:

I:0
I:3
I:2
I:1
I:4

根据执行结果可以看出,newWorkStealingPool 是并行处理任务的,并不能保证执行顺序。

7. 总结

Executors 可以创建 6 种不同类型的线程池,其中

  • newFixedThreadPool() 适合执行单位时间内固定的任务数

  • newCachedThreadPool() 适合短时间内处理大量任务

  • newSingleThreadExecutor() 和 newSingleThreadScheduledExecutor() 为单线程线程池。它们的区别在于:

    • newSingleThreadExecutor() 创建的线程池去执行任务时,都是一次性的,而

    • newSingleThreadScheduledExecutor() 可以执行周期性的任务。

  • newWorkStealingPool() 为 JDK 8 新增的并发线程池,可以根据当前电脑的 CPU 处理数量生成对比数量的线程池,但它的执行为并发执行不能保证任务的执行顺序。

8. 其它

『强制』线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 各个方法的弊端:

  1. newFixedThreadPool 和 newSingleThreadExecutor :

    主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM 。

  2. newCachedThreadPool 和 newScheduledThreadPool :

    主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM 。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值