简单明了的Java线程池
线程池
线程池从功能上来看,就是一个任务管理器。在Java
中,Executor
接口是线程池的根接口,其中只包含一个方法:
Executor
void execute(Runnable command); // 执行任务
ExecutorService
继承了Executor
接口,提供了一些线程池的基础方法:
void shutdown(); // 关闭线程池(不接受新任务,但是旧任务会执行)
List<Runnable> shutdownNow(); // 关闭线程池,返回待执行任务
boolean isShutdown(); // 线程池是否会关闭
boolean isTerminated(); // 关闭之前所有任务是否被关闭。(必须先执行过shutdown)
....
再往下是两种线程池的实现:ThreadPoolExecutor
和ForkJoinPool
。ThreadPoolExecutor
中维护了一个BlockingQueue
阻塞队列保存所有的待执行任务,而ForkJoinPool
中每一个线程都有自己的BlockingQueue
用来存储任务。
ThreadPoolExecutor
在ThreadPoolExecutor
的构造方法中,需要提供几个参数:corePoolSize
、maximumPoolSize
、keepAliveTime
、BlockingQueue
、RejectedExecutionHandler
。其中corePoolSize
表示当前线程池维护几个线程,maximumPoolSize
表示允许的最大线程数。keepAliveTime
表示如果当前线程数在
corePoolSize
和maximumPoolSize
之间时,允许在多久时间内保持存活等待新任务。BlockingQueue
是保存任务的阻塞队列,RejectedExecutionHandler
是不同的拒绝策略。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
那么
ThreadPoolExecutor
中是如何创建新线程呢?
在接到请求执行一个新任务时,首先会判断当前线程数是否大于corePoolSize
,如果没有则创建新线程。否则将当前任务放到阻塞队列中,如果当前队列已满,则创建新的线程执行任务。在成功将当前任务放到队列中之后,我们还需要二次判断当前线程池中是否有线程已经销毁或者当前线程池停止运行。当线程数量大于maximumPoolSize
时,执行拒绝策略。
这段代码在ThreadPoolExecutor
的excute
方法中体现,其中也有详细地对执行任务顺序的描述。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
如何设置线程池的线程数量:如果是CPU密集型应用,则线程大小为N或者N+1;如果是IO密集型应用,则线程大小为2N或者2N+2
ForkJoinPool
ForkJoinPool
和ThreadPoolExecutor
都是继承自AbstractExecutorService
抽象类,所以它和ThreadPoolExecutor
的使用几乎没有多少区别,除了任务变成了ForkJoinTask
以外。
ForkJoinPool
和ThreadPoolExecutor
最主要的区别就是ForkJoinPool
中每一个线程都有属于自己的队列,当某个线程队列任务全部执行完了时,会通过"窃取工作"从别的线程队列中取出一个任务进行执行。
具体的策略就是每一个线程维护一个自己的队列,先进后出(FILO
)将任务塞到队列的头部,执行任务时从队列头部取出任务执行。其他线程从队列尾部窃取任务执行。减少阻塞消耗,特别适用于计算型任务。
Callable和Future
在jvm内存模型中我们会发现,每个线程有自己的内存空间,而且线程的run
方法返回空值。这个时候就会出现一个问题,如果想要在其他线程中拿到当前线程运行的结果是不可能的。所以就有了Callable
和Future
的存在,Callable
可以让线程返回值,而Future
可以拿到线程的返回值。
这里有一个误区,Callable
的使用不是在线程的run
方法中将返回值传递给call
方法,再从future.get()
中取值。实际上Callable
和Future
的配合使用,是利用了一个叫做FutureTask
的类,这个类同时继承了Runnable
和Future
接口,初始化的构造函数中会接受一个Callable
对象或者将Runnable
对象封装到Callable
对象中。在他的run
方法中,会调用call
方法。
public void run() {
if (state != NEW ||
!RUNNER.compareAndSet(this, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable; // 获取传入的callable对象
if (c != null && state == NEW) {
V result; // call返回的结果
boolean ran;
try {
result = c.call(); // 执行call方法中的任务
ran = true; // 成功执行
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result); // 返回结果值
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
实际上这个set(result)
会将返回的结果塞到private Object outcome
对象中,这个就是我们最终通过future.get()
获取到的值。
RejectedExecutionHandler
上述线程池中我们提到拒绝策略,就是在线程池满时拒绝添加新线程,执行拒绝策略,就是通过这个RejectedExecutionHandler
进行处理。在ThreadPoolExecutor
线程池中,定义了4种不同的拒绝策略,他们都继承了RejectedExecutionHandler
。
- AbortPolicy
这个是默认的拒绝策略,他将丢弃当前的任务,并抛出异常。
/**
* A handler for rejected tasks that throws a
* {@link RejectedExecutionException}.
*
* This is the default handler for {@link ThreadPoolExecutor} and
* {@link ScheduledThreadPoolExecutor}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
他对当前的任务r
没有做任何处理。这是默认的拒绝策略,没有固定的使用场景,但是有一点需要注意,Executors
中提供的几种线程池的队列都是无界的,所以不会触发拒绝策略。
- DiscardPolicy
丢弃当前任务,不抛出异常。
/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
他既没有执行任务,也没有抛出异常。这种拒绝策略一般是你的任务无关紧要时使用,因为他不会返回异常。
- DiscardOldestPolicy
将当前队列头(即将被执行的任务)丢弃,执行当前任务。
/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
在这个拒绝策略中,他将当前队列头丢弃,调用e.execute(r)
重新尝试执行当前任务,当然他也会悄无声息的丢弃任务。这种一般是新任务的优先度更高时使用,比如说新消息来了,那么旧消息就无关紧要了。
- CallerRunsPolicy
在当前线程下运行该任务。
/**
* A handler for rejected tasks that runs the rejected task
* directly in the calling thread of the {@code execute} method,
* unless the executor has been shut down, in which case the task
* is discarded.
*/
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
在当前线程执行该任务,相当于做了一个阻塞,当前线程暂停接受新任务并且当前线程不处于空闲状态,但是如果该任务执行时间过长可能会导致其他线程等待。这种一般是在不允许失败的、并发量较小的情况下使用。
工具类
Executors
Executors
是线程池工具类和工厂类,最主要的是提供了几种常见的线程池的创建。jdk8
以前一共是4种,而在1.8之后添加了forkjoin
连接池。
newSingleThreadExecutor
:这是单例的线程池,只包含一条线程,如果该线程意外中断,会创建新线程。newFixedThreadPool
:固定数量线程池,由于是固定大小的线程池,所以maximumPoolSize是没有意义的,只会创建corePoolSize个线程。newCachedThreadPool
:缓存线程池。corePoolSize
大小为0,maximumPoolSize
为Integer.MAX_VALUE
,60秒内无任务则销毁线程。ScheduledThreadPoolExecutor
:定时线程池。可以设置延时时间。
在jdk8
之后,Executors
添加了一种forkjoinpool
线程池:
newWorkStealingPool
: 工作窃取线程池,通过ForkJoinPool
实现。每个线程有独立的队列,完成之后从别的线程窃取工作。
当然除了除了上述之外,也可以通过ThreadPoolExecutor
或者ForkJoinPool
的构造函数来自定义线程池。
Semaphore
Semaphore
叫做信号量,我们可以通过设置个数来限制同时进入资源的线程数。比如一个停车场有十个停车位,我们设置停车位为10,那么同时只能有10辆车进入停车场。
Semaphore
的初始化过程中,我们可以提供两个参数,permits
和fair
分别表示信号量的个数和是否公平,如果fair
为true的话会使用公平锁的机制初始化线程队列同步器(AQS
)。公平锁和非公平锁的区别在于公平锁线程等待的时间越长越优先,所以公平锁的吞吐量比非公平锁小,非公平锁可能会造成某个线程等待时间过长。
在Semaphore
两种锁的具体实现就是在公平锁的tryAcquireShared
方法中多了一句:if (hasQueuedPredecessors()) return -1;
如果当前线程不是排在最前面的话,就返回-1;
Semaphore
使用acquire
方法来获取信号,通过release
方法在结束后释放信号。他的具体实现就是通过AQS
中的state
状态来保存promits
数量,被拿走就减去指定的数量。
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors()) // 判断当前线程是否在队列最前面
return -1;
int available = getState(); // 获取AQS中保存的promits数量
int remaining = available - acquires; // 总promits减去当前要取的信号量
if (remaining < 0 ||
compareAndSetState(available, remaining)) // CAS(compare and swap)乐观锁的方式更换state值
return remaining; // 返回剩下的信号量个数
}
}
可能看到这里会对AQS的概念比较模糊,如果想要进一步了解AQS的话,可以直接跳到AQS的部分,因为接下来的东西很多都跟AQS有关。
CountDownLatch
CountDownLatch
一般叫做计数器,他的作用是挂起线程等待其他线程运行到计算器清0之后再继续运行。一般用于流程控制,等待前置线程执行。
他也是通过AQS
来进行实现的,构造函数中要求提供一个count
值赋值给state
,调用countDown
之后计数器减1,直到state值
为0时,才能获取锁,继续执行下面的任务。
public static void main(String[] args) throws InterruptedException, ExecutionException {
CountDownLatch countDownLatch = new CountDownLatch(2);
Runnable taskMain = () -> {
try {
countDownLatch.await(); // 挂起,等待AQS的state值为0时被唤醒,解锁继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("继续执行任务");
};
Runnable task1 = () -> {
countDownLatch.countDown(); // 将AQS的state值减1 (计数器减1)
System.out.println("前置任务1完成");
};
Runnable task2 = () -> {
countDownLatch.countDown(); // 将AQS的state值减1 (计数器减1)
System.out.println("前置任务2完成");
};
new Thread(taskMain).start();
new Thread(task1).start();
new Thread(task2).start();
}
用法也很简单,在前置线程中使用countDown
计数减1,在后续线程中使用await
等待锁释放。
锁
AQS
AbstractQueuedSynchronizer
是concurrent
包下非常重要的一个接口,ReentranLock
、Semaphore
、CountDownLatch
等工具类的底层都是通过AQS
来实现。
那么AQS
底层是怎么来进行实现的呢?不同于synchronized
直接作用于jvm
底层,AQS
定义了一个volatile
属性的变量state
来表示当前资源是否被锁。那么如何在多线程下对一个变量进行修改?这里借用了乐观锁的概念,采用CAS
的方式来修改变量。CAS
指的是compare and swap
,它假设所有线程对资源的访问是没有冲突的,如果有冲突,则通过比较交换的方式来解决。如果要修改,则会传两个值,一个是state
的预期值,一个是修改之后的值,如果预期值跟state
值一致,那么就允许更改,否则则判断是有别的线程已经对state
修改过了,表示锁定状态。这个时候当前线程会被挂起,放到CLH队列中,等待其他线程释放state
唤醒。
简单明了的Java线程池的更多相关文章
- 含源码解析,深入Java 线程池原理
从池化技术到底层实现,一篇文章带你贯通线程池技术. 1.池化技术简介 在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能. 在编程领域,比较典型的池化技术有: 线程池.连接池.内存池 ...
- 这么说吧,java线程池的实现原理其实很简单
好处 : 线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配.调优和监控,有以下好处: 1.降低资源消耗: 2.提高响应速度: 3.提高线 ...
- Java并发编程:Java线程池核心ThreadPoolExecutor的使用和原理分析
目录 引出线程池 Executor框架 ThreadPoolExecutor详解 构造函数 重要的变量 线程池执行流程 任务队列workQueue 任务拒绝策略 线程池的关闭 ThreadPoolEx ...
- Netty核心概念(7)之Java线程池
1.前言 本章本来要讲解Netty的线程模型的,但是由于其是基于Java线程池设计而封装的,所以我们先详细学习一下Java中的线程池的设计.之前也说过Netty5被放弃的原因之一就是forkjoin结 ...
- Java线程池使用说明
Java线程池使用说明 转自:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极 ...
- Java线程池的几种实现 及 常见问题讲解
工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...
- Java线程池的原理及几类线程池的介绍
刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...
- [转 ]-- Java线程池使用说明
Java线程池使用说明 原文地址:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1. ...
- 从使用到原理学习Java线程池
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
随机推荐
- ElasticSearch简介和快速实战
ElasticSearch简介和快速实战 ElasticSearch与Lucene Lucene可以被认为是迄今为止最先进.性能最好的.功能最全的搜索引擎库(框架) 但是想要使用Lucene,必须使用 ...
- TCP通信简单梳理
一.什么是TCP协议 TCP协议是一种面向连接的可靠的通信协议,最重要的两个特点:连接.可靠. 二.TCP是如何进行通信的 TCP通过三次握手建立连接后客户端服务端的内核都分别开辟资源,这时候开始进行 ...
- VUE 之_this.da 和 this
作者:张艳涛 日期:2020-07-29 this用在第一层,如果在JS第3二层,要用 _this importfxx(obj) { let _this = this; let inputDOM = ...
- Spring Boot中使用时序数据库InfluxDB
除了最常用的关系数据库和缓存之外,之前我们已经介绍了在Spring Boot中如何配置和使用MongoDB.LDAP这些存储的案例.接下来,我们继续介绍另一种特殊的数据库:时序数据库InfluxDB在 ...
- CentOS 7 安装虚拟机
1.本次安装centos7 安装使用的软件是VitrualBox 虚拟机软件 Oracle公司的虚拟机软件,免费商品(大家可以百度搜索去官网下载) 1:我这里使用的是阿里的centos7的镜像(大家可 ...
- Vue-Router 详细解析学习
首先还是先创项目 然后选择vue-router 再创建 再敲代码学习前,我们先了解什么是路由? 路由有一个非常重要的概念就是路由表: 本质就是一个映射表,决定数据的指向. 我们生活中常常听到的路由器, ...
- Windows影子用户创建与3389连接
#当获得一条shell后,可以创建一个影子用户,通过影子用户可以行驶正常用户的所有权限与功能,并且只可在注册表中被检测出来---(应急响应注册表很重要) 1.首先需要拥有权限创建一个Administr ...
- 『Java』List Set
观前提醒:本文内容多为入门时的学习笔记,笔记内容有些混乱!!! | | | | | | | | | | | | 泛型只能是引用类型,不能是基本类型. 如果希望集合中存储的是基本类型数据,需要基本类型对 ...
- 一键部署lamp脚本
#!/bin/bash systemctl stop firewalld systemctl disable firewalld setenforce 0 #-------Apache------ # ...
- Git-02-版本回退
环境准备 1 修改readme.txt内容 Git is a distributed version control system. Git is free software. 2 git statu ...