在现代java开发中,多线程是一个绕不开话题。如果绕开了,估计也走远了。。。远离java,珍爱生命!
这里就来探讨一下java线程池的使用及实现。
热身
先看一个多线程例子
import org.junit.Test;
public class ThreadPoolExecutorTest {
@Test
public void test() {
// 显示创建一个线程
Thread t = new Thread(new Runnable() {
// 执行任务
@Override
public void run() {
System.out.println("hello world");
}
});
// 启动线程
t.start();
// 主线程打印
System.out.println("this is main thread");
}
}
执行之后,看下打印结果
假设现在要同时执行100个任务,怎么办?要不开启100个线程试试?改写上面的程序如下
import org.junit.Test;
public class ThreadPoolExecutorTest {
@Test
public void test() {
for (int i = 0; i < 100; i++) {
// 显示创建一个线程
Thread t = new Thread(new Runnable() {
// 执行任务
@Override
public void run() {
System.out.println("hello world");
}
});
// 启动线程
t.start();
}
// 主线程打印
System.out.println("this is main thread");
}
}
那上面的程序是否真的能线性提高程序的执行效率呢?很遗憾,一般情况下,是没有做到的。
原因有2点:
- 首先,上面的过程中,显示创建一个新线程,执行完成后,销毁线程,都会产生比较大的开销
- 其次,在创建线程的过程中,任务处于等待。
如果提前创建好线程,那么任务到达时,是不是就不用等待,直接将由线程执行。
而且也省去了线程创建和销毁的巨大开销。
什么是线程池
从名字来看,大胆猜测是指管理一组工作线程的资源池。就是说,提前创建好一批线程,放入其中,等有任务过来,直接获取一个线程执行任务,任务执行完成,线程放回去。所以这时涉及到一个任务队列。
任务队列:保存所有等待执行的任务。
线程池:保存执行任务的线程。
默认线程池
jdk1.5之后,引入了concurrent包,里面添加了很多强大的并发功能。
其中就包括对线程池的默认实现
- 固定大小的线程池:newFixedThreadPool,每当提交一个任务就创建一个线程,直到达到最大,这时线程的规模不再发生变化
@Test public void fixedThreadPoolTest() { int threadNum = 10; // 固定大小的线程池 ExecutorService executor = Executors.newFixedThreadPool(threadNum); executor.execute(new Runnable() { @Override public void run() { System.out.println("hello world"); } }); }
- 可缓存的线程池:newCachedThreadPool,如果需求超过线程池的规模,处理不过来,就会创建新的线程。如果线程空闲,将会被回收,线程池大小没有限制
@Test public void cachedThreadPoolTest() { // 可缓存的线程池 ExecutorService executor = Executors.newCachedThreadPool(); executor.submit(new Runnable() { @Override public void run() { System.out.println("hello world"); } }); }
- 单一线程:这是一种特殊的newFixedThreadPool,它创建单个工作者线程来处理任务,如果这个线程异常结束,会新建线程来补充。这种方式能确保任务按照队列顺序执行
@Test public void SingleThreadPoolTest() { // 单一的线程 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(new Runnable() { @Override public void run() { System.out.println("hello world"); } }); }
一般来说,都可以用上面的默认线程池,不过在某些场景下它们或多或少存在一些问题
分别看下上面几个线程池的静态方法
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
仔细看下上面的几个方法,发现了什么没有?
都是ThreadPoolExecutor的同一个构造方法,只是参数不同。
所有线程池的底层都是依赖于ThreadPoolExecutor类,基于它,甚至可以自定义线程池。
线程池的核心参数
线程池的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
各个参数的意义
- corePoolSize:线程池的核心线程数
- maximumPoolSize:最大线程数,如果任务过多,超过核心线程数的处理能力,就会创建新的线程执行任务,这个参数,代表所能创建的最大线程的个数。
- keepAliveTime:线程存活时间。在上面的情况中,如果创建的线程数量超过了核心线程数,并且在keepAliveTime时间段内,没有新的任务执行,就回把多余的线程进行回收。
- unit:时间单位。
- workQueue:任务队列。如果线程中的线程处理任务繁忙,此时任务就将堆积到任务队列里面,线程处理时,从队列里面取出执行
- RejectedExecutionHandler:拒绝策略。如果使用了有界队列,任务达到了最大值,再有任务到达时,该怎么做呢?这时就涉及到拒绝队列
流程图
类图
线程创建与任务执行流程
先声明一个任务
class ThreadPoolTestTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
写个测试方法
@Test
public void testThreadPoolParamsTest() {
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
ExecutorService executor = new ThreadPoolExecutor(2, 5, 60,
TimeUnit.SECONDS, queue);
// 提交任务数10个,此时10 > 1 + 2 + 7,超过队列大小,将会执行拒绝策略,抛出异常
for (int i = 0; i < 16; i++) {
executor.execute(new Thread(new ThreadPoolTestTask(), "线程" + i));
// System.out.println("线程池中活跃线程数" + ((ThreadPoolExecutor) executor).getActiveCount());
System.out.println("线程池中活跃线程数" + ((ThreadPoolExecutor) executor).getPoolSize());
if (queue.size() > 0) {
System.out.println("=================================队列中阻塞的任务数" + queue.size());
}
}
}
执行下,输出结果
线程池中活跃线程数1
线程池中活跃线程数2
线程池中活跃线程数2
=================================队列中阻塞的任务数1
线程池中活跃线程数2
=================================队列中阻塞的任务数2
线程池中活跃线程数2
=================================队列中阻塞的任务数3
线程池中活跃线程数2
=================================队列中阻塞的任务数4
线程池中活跃线程数2
=================================队列中阻塞的任务数5
线程池中活跃线程数2
=================================队列中阻塞的任务数6
线程池中活跃线程数2
=================================队列中阻塞的任务数7
线程池中活跃线程数2
=================================队列中阻塞的任务数8
线程池中活跃线程数2
=================================队列中阻塞的任务数9
线程池中活跃线程数2
=================================队列中阻塞的任务数10
线程池中活跃线程数3
=================================队列中阻塞的任务数10
线程池中活跃线程数4
=================================队列中阻塞的任务数10
线程池中活跃线程数5
=================================队列中阻塞的任务数10
java.util.concurrent.RejectedExecutionException: Task Thread[线程15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@626b2d4a[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.example.threads.chapter1.yield.ThreadPoolExecutorTest.testThreadPoolParamsTest(ThreadPoolExecutorTest.java:106)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Process finished with exit code -1
核心线程2,最大线程5,队列10所以线程池最多有5+10,15个任务存在。当后面再进任务时,将执行默认的拒绝策略,抛出异常
其中线程池与队列关系如下
一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
拒绝策略
当有界队列被填满后,拒绝策略将发生作用
在上面我们看到拒绝策略:defaultHandler
看下代码,默认的拒绝策略
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
线程池除了默认的拒绝策略以外,还有另外3个,一共有4种
- AbortPolicy:也是默认的策略。如果队列超过最大值,将抛出RejectedExecutionException异常,调用方可以捕获该异常进行处理
- DiscardPolicy:如果队列超过最大值,新来的任务会被悄悄丢弃
- DiscardOldestPolicy:如果队列超过最大值,会丢弃下一个将被执行的任务,并将新任务提交到队列中
- CallerRunsPolicy:实现了调用者运行的一种机制。即这种策略下,即不会丢弃任务,也不会抛出异常,并将任务在调用方的线程中执行(不会在线程池中执行)
可以看下对应的类图
验证一下,自定义一个线程池,核心线程数为1,最大线程数为2,队列长度为7,这时候如果提交的任务个数超过队列长度,将会抛出异常
@Test
public void testAbortRejecttion() {
// 自定义线程池
// 核心线程1,最大线程2,队列数为7
ExecutorService executor = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(7));
// 提交任务数10个,此时10 > 1 + 2 + 7,超过队列大小,将会执行拒绝策略,抛出异常
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
});
}
}
运行上面的程序,结果如下
这个异常就是执行拒绝策略方法时抛出的
现在换成丢弃策略DiscardPolicy来试试
@Test
public void testDiscardRejection() {
ExecutorService executor = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(7), new ThreadPoolExecutor.DiscardPolicy());
// 提交任务数10个,此时10 > 1 + 2 + 7,超过队列大小,将会执行拒绝策略,抛出异常
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
});
}
}
执行一下,发现正常执行,但是会发现,其它只有9个任务执行了,因为超过队列的长度的任务被丢弃了,其它主要是它的拒绝策略方法,新任务来了,什么都不作
丢弃最早的类似如下,只不过是把最早的任务取出但执行最新的任务
下一篇文章中,分析一下,线程池执行的源码