最近看晓说,发现一个比较有意思的人物,就是日本战国时代的历史人物织田信长,说到织田信长,第一感觉就是光荣公司的游戏系列,信长之野望和三国志,头像和曹操比较像,日本的美男子类似那种高鼻梁吧,所以曹操沾光了,织田信长和曹操的性格比较像,都是那种为达目的不择手段的狠人,叫他小曹操也可以,说到日本家喻户晓的历史事件——本能寺之变,就是织田信长被刺杀了,和我们三国时代的赤壁之战一样家喻户晓,但是赤壁之战曹操没凉,被关羽放了。日本历史还是比较有意思的,有兴趣可以去了解。反正很有趣,日本历史上就织田信长想取代天皇,其他的都不想取代,甚至打仗都打的稀里糊涂的,组织刺杀织田信长的人也不是为了篡位,最后被织田信长的大将丰臣秀吉打败,逃跑的过程中是被一个小农民杀了,就是看看中他身上的布料和铁和骑着的马,反正我是懵了。
目录
- 为什么使用线程池
- 使用线程池的风险
- 有效使用线程池的准则
- ThreadPoolExecutor
- 线程池的处理流程和原则
- 线程池的种类
- 总结
1. 为什么使用线程池
在编程过程中经常会使用线程来处理一些异步任务,每个线程的创建和销毁都是需要一定的资源的,如果每次执行任务都需要开启一个新线程去执行,那么这些线程的创建和销毁将消耗大量资源,并且线程都是“各自为政”的,很难对其控制,更别说是有一堆线程在执行了,这时候就会用到我们的线程池。
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
人话:就是你用的时候线程池已经给你创建好了,你直接用就是,多了的时候就不让你创建了,让你等,这样可以防止资源不足,就是你如果访问n次,资源你都要了,占满了,我app还要运行吗?
2. 使用线程池的风险
用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
2.1 死锁
什么是死锁:当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了
第一种情况:线程a我锁了x在等y,线程b我锁了y在等x,除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
第二种情况:就是只要池任务开始了无限期阻塞,其目的是等待一些资源或条件,此时只有另一个池任务的执行才能使那些条件成立。除非能保证线程池足够大,否则会发生线程饥饿死锁。,什么意思?我解释一下,创建两个任务 Task1 和 Task2,其中 Task1 从队列中取出元素, Task2 向队列添加元素。其中,队列为阻塞队列,当队列为空时,Task1 将会一直阻塞等待 Task2 执行,但是此时只有一个线程只能执行一个任务,所以这个 Task1 将会永远阻塞,Task2 将永远无法执行。这就是任务之间相互依赖的饥饿死锁。
第二种情况你们可能不太理解,付个代码,可以试试
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadDeadLockTest{
//创建一个阻塞队列
private static BlockingQueue Q = new ArrayBlockingQueue(10);
//线程池线程数量
private static final int THREAD_SIZE = 1;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//创建一个固定线程的线程池
ExecutorService service = Executors.newFixedThreadPool(THREAD_SIZE);
service.submit(new Task1());
service.submit(new Task2(1));
service.shutdown();
}
//任务1取出阻塞队列的值并打印
static class Task1 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("线程一开始");
//取出阻塞队列的值,如果没有则会阻塞
int value = (int) Q.take();
System.out.println("线程一结束, value=" + value);
return null;
}
}
//任务2,往阻塞队列增加元素
static class Task2 implements Callable {
private int val;
public Task2(int value) {
val = value;
}
@Override
public Object call() throws Exception {
System.out.println("线程二添加,value=" + val);
//往阻塞队列增加元素
Q.put(1);
return null;
}
}
}
运行结果
Task1 is running
可以看出只运行了线程一,线程二都没开始。
线程池足够大,才能避免饥饿死锁的发生。所以,我们把上面的代码的线程数量加大一点试试也就是THREAD_SIZE=1改为THREAD_SIZE =2,就能有效的避免这种情况了
2.2 资源不足
如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。
2.3 并发错误
线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心。而最好使用现有的、已经知道能工作的实现,例如 util.concurrent 包。
2.4 线程泄漏
各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。
2.5 请求过载
3. 有效使用线程池的准则
- 不要对那些同步等待其它任务结果的任务排队
这可能会导致上面所描述的那种形式的死锁,在那种死锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因为所有的线程都很忙。 - 在为时间可能很长的操作使用合用的线程时要小心
如果程序必须等待诸如 I/O 完成这样的某个资源,那么请指定最长的等待时间,以及随后是失效还是将任务重新排队以便稍后执行。这样做保证了:通过将某个线程释放给某个可能成功完成的任务,从而将最终取得某些进展。 - 理解任务
要有效地调整线程池大小,您需要理解正在排队的任务以及它们正在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果您有不同的任务类,这些类有着截然不同的特征,那么为不同任务类设置多个工作队列可能会有意义,这样可以相应地调整每个池。
4. ThreadPoolExecutor
它的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
参数 | 作用 |
---|---|
corePoolSize | 线程池核心线程数 |
maximumPoolSize | 线程池最大数 |
keepAliveTime | 空闲线程存活时间 |
unit | 时间单位 |
workQueue | 线程池所使用的缓冲队列 |
threadFactory | 线程池创建线程使用的工厂 |
handler | 线程池对拒绝任务的处理策略 |
handler默认的应对策略是AnordPolicy还有其它几种如下
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:也是丢弃任务,但是不抛出异常。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
CallerRunsPolicy:由调用线程处理该任务
这些参数的设置用法就比较官方了,我说一些这个类的特性
特性一:当池中正在运行的线程数(包括空闲线程)小于corePoolSize时,新建线程执行任务。
特性二:当池中正在运行的线程数大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行。
特性三:当队列里的任务数达到上限,并且池中正在运行的线程数小于maximumPoolSize,对于新加入的任务,新建线程。
特性四:当队列里的任务数达到上限,并且池中正在运行的线程数等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)。
这个类还是比较好用的,给个demo测试用
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadDeadLockTest{
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 60L,TimeUnit.SECONDS, new LinkedBlockingQueue(1));
// 任务1
pool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3 * 1000);
System.out.println("任务1,运行我的线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 任务2
pool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
System.out.println("任务2,运行我的线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 任务3
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务3,运行我的线程是:" + Thread.currentThread().getName());
}
});
// 任务4
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务4,运行我的线程是:" + Thread.currentThread().getName());
}
});
}
}
运行结果是:
任务4,运行我的线程是:pool-1-thread-3
任务3,运行我的线程是:pool-1-thread-3
任务1,运行我的线程是:pool-1-thread-1
任务2,运行我的线程是:pool-1-thread-2
5. 线程池的处理流程和原则
付图:
主要分为三步
- 提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到线程数,则创建核心线程处理任务;否则,就执行下一步;
- 接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执行下一步;
- 接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出RejectedExecutionException异常。
不明白没关系,我们再来看看线程池的处理流程图:
在看看它的处理情况
情况一:调用ThreadPoolExecutor的execute提交线程,首先检查CorePool,如果CorePool内的线程小于CorePoolSize,新创建线程执行任务。
情况二:如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到BlockingQueue。
情况三:如果不能加入BlockingQueue,在小于MaxPoolSize的情况下创建线程执行任务。
情况四:如果线程数大于等于MaxPoolSize,那么执行拒绝策略。
6. 线程池的种类
6.1 newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
- 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
- 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪,但适用立即处理耗时较小的任务。
示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
}
}
6.2 newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源,就是核心线程不会被回收。
示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
定长线程池的大小最好根据系统资源进行设置如Runtime.getRuntime().availableProcessors()。
6.3 newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
6.4 newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
延迟3秒执行,延迟执行示例代码如下:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Created by syt on 2019/4/16.
*/
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
}
}
7. 总结
其实第六节这些线程池的种类其中的一些细节没有介绍给你们,希望你们能够自己去深入一下,写这个博客真的是花了我很长时间,我自己这个线程池真的没怎么用过,但是学到它,我就把网上和书上的都总结出来,分享给大家,希望对初学者有点点帮助,实在写的不太好,就当自己的笔记吧。这里也是借鉴了很多大佬的文章和图片,如有侵权联系我,我delete,如有不足请您指出,评论留言或者联系QQ2714730493
给自己一个恰如其分的自信。