线程池一个并不陌生的概念,印象中的线程池经常使用,但是却不怎么了解原理。本文主要从线程出发,讲解线程池的使用,以及线程池的底层原理。

线程

创建线程的方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口通过FutureTask包装器来创建Thread线程

但是从本质上来讲,java中创建线程的方式只有一种,就是实现Runable接口,即所以的线程都实现了run()方法。

下面可以通过java中的UML图进行证明

【JUC】——深入浅出搞懂线程池_线程池

【JUC】——深入浅出搞懂线程池_java_02

先看个线程的使用demo

public class ThreadTest {

public static class ThreadDemo extends Thread {
private String name;

public ThreadDemo(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println("调用方式:" + this.name + ",线程id:" + Thread.currentThread().getId() + ",线程name:" + Thread.currentThread().getName());
}
}

public static void main(String[] args) {
System.out.println("main线程id:" + Thread.currentThread().getId());
//线程级别
new ThreadDemo("start").start();
//方法级别
new ThreadDemo("run").run();
}
}

执行结果

【JUC】——深入浅出搞懂线程池_线程池_03

通过执行结果可以看出,调用start()和 run()方法执行的线程并不一致,start()是新创建的线程,而run()则和main()共用一个线程。它们之间线程的关系,如下图

【JUC】——深入浅出搞懂线程池_并发编程_04

1、线程有生命周期

2、调用start(),执行run() --》多线程

3、start()是线程级别,而单独调用run()是方法级别,等同于普通调用

线程池

public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("使用Executors.newSingleThreadExecutor 花费时间:"+(System.currentTimeMillis() - start));
System.out.println("大小:"+list.size());

System.out.println("==================");

list.clear();
for (int i = 0; i < 100000; i++) {
Thread thread = new Thread() {
@Override
public void run() {
list.add(random.nextInt());

}
};
thread.start();
thread.join();
}
System.out.println("使用thread时间:"+(System.currentTimeMillis() - start));
System.out.println("大小:"+list.size());

}

执行结果

【JUC】——深入浅出搞懂线程池_多线程_05

 为什么两种方式执行结果相差这么大?

因为方式一使用的是线程池的方式,那么为何线程池?

        线程池 就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

两种处理方式时间相差较大的原因,本质上就是省去线程的创建时间,线程的复用,节省了大量的时间消耗。

几种常见的线程池使用

public class ThreadPoolDemo {
public static void main(String[] args) {
//固定大小的线程池,跟回收型线程池类似,只是可以限制同时运行的线程数量 执行慢
ExecutorService executorService1 = Executors.newFixedThreadPool(10);
//回收型线程池,可以重复利用之前创建过的线程,运行线程最大数是Integer.MAX_VALUE,快
ExecutorService executorService2 = Executors.newCachedThreadPool();
//单线程池,同时只有一个线程在跑,最慢
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
//提供延迟和定时任务的线程池
Executors.newScheduledThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService1.execute(new MyTask(i));
}
}
}

class MyTask implements Runnable {
int i = 0;

public MyTask(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--" + i);
try {
Thread.sleep(1000L);
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行结果已经标注到注释中,就不放图单独展示了。

那么为什么每种线程池的执行效率又各不相同呢?

【JUC】——深入浅出搞懂线程池_线程池_06

在ThreadPoolExecutor的创建过程涉及到以下几个属性值

int corePoolSize,核心线程
int maximumPoolSize,非核心线程
long keepAliveTime,时间
TimeUnit unit,时间单位
BlockingQueue<Runnable> workQueue,队列
ThreadFactory threadFactory,线程工厂
RejectedExecutionHandler handler 拒绝策略

先看线程池组成

【JUC】——深入浅出搞懂线程池_线程池_07

核心线程在整个程序运行过程会一直存在,即使处于空闲。

非核心线程等于maximumPoolSize减corePoolSize,非核心线程在空闲状态中的存活时间取决于keepAliveTime单位秒,如果keepAliveTime为0,即无待执行任务后,非核心线程立刻结束生命周期。

队列用于存放尚未处理的任务

RejectedExecutionHandler拒绝策略,当核心线程,队列,和非核心线程都没有空间存放待处理任务时,则任务会被拒绝执行。ps:拒绝策略非本文核心,只做简单展示

【JUC】——深入浅出搞懂线程池_并发编程_08

下面结合demo来展示线程池中核心线程,非核心线程,队列的使用

public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(2));
for (int i = 0; i < 6; i++) {
threadPoolExecutor.execute(new MyTask(i));
}
}
}

class MyTask implements Runnable {
int i = 0;

public MyTask(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--" + i);
try {
Thread.sleep(1000L);
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行结果

【JUC】——深入浅出搞懂线程池_java_09

为什么任务2,3比任务4,5执行的更晚?

原因在线程池中任务的提交顺序和任务的执行顺序并不相同

提交顺序

对应到java代码中java.util.concurrent.ThreadPoolExecutor#execute

【JUC】——深入浅出搞懂线程池_ide_10

因为这个提交顺序,所以在java提供的线程池FixedThreadPool没有非核心线程,因为使用队列为LinkedBlockingQueue:是一个基于链表结构的阻塞队列并无边界,此时即使有非核心线程也不会发挥作用。同样CachedThreadPool,使用的是SynchronousQueue一个不存储元素的阻塞队列,所以CachedThreadPool执行效率特别高,因为任务不会堆积,会一直创建线程来执行新的任务。

但是在日常开发过程上述几种线程池均不推荐使用,因为LinkedBlockingQueue无边界,如果任务过多,会发生内存耗尽的情况。而CachedThreadPool当任务过多的时候,会创建大量线程,进行任务处理,同样会占用过多的CPU资源阻碍其他进程。

执行顺序

从执行结果其实就可以看出执行顺序为核心线程-》非核心线程-》队列

线程的本质都有run()->runWorker()

java.util.concurrent.ThreadPoolExecutor#runWorker

【JUC】——深入浅出搞懂线程池_java_11

为什么任务0,1和任务2,3执行的线程是相同的?

这个问题的本质,线程是如何做到复用的

线程复用和线程执行流程图:

总结 

       线程有三种创建方式,但是本质上只有一种即实现Runable接口,所以线程均有run()。想要搞懂线程池的底层实现原理,必须先明白一个概念,start()是创建线程,而run()则只是普通方法。在线程复用的时候,则调用的是方法级别的run()。

        线程池的有核心线程,非核心线程,队列三个属性值。在向线程池提交任务和执行任务的时候顺序并不相同,提交任务顺序为核心-》队列-》非核心,而执行任务则为核心-》非核心-》队列。