一、操作系统中线程和进程的概念
现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一般我们说一个进程开始运行,是指该进程的主线程开始运行。一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
“同时”执行是人的感觉,在线程之间实际上轮换执行。
二、Java中的线程
所以一般我们在java中都频繁使用线程池,实现线程的复用。
一、创建java线程的4种方法
Java 使用 Thread 类代表线程,所有的线程对象必须是 Thread 类或其子类的实例。Java 可以用四种方式来创建线程,如下:
①继承 Thread 类创建线程没有返回值
;
②实现 Runnable 接口创建线程没有返回值
;
③实现 Callable 接口,通过 FutureTask 包装器来创建 Thread 线程有返回值
;
④线程池:使用 ExecutorService、Callable、Future 实现有返回结果的线程有返回值
。
1️⃣------------------------继承Thread类创建线程---------------------
- 定义 Thread 类的子类,并重写该类的 run(),该方法的方法体就是线程需要完成的任务,run() 也称为线程的执行体。
- 创建 Thread 子类的实例,也就是创建了线程对象。
- 启动线程,即调用线程的 start()。
代码实例:
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
2️⃣------------------------实现Runnable接口创建线程---------------------
- 定义 Runnable 接口的实现类,同样要重写 run()。这个 run() 和 Thread 中的 run() 一样是线程的执行体。
- 创建 Runnable 实现类的实例,并用这个实例作为 Thread 的 target 来创建 Thread 对象,这个 Thread 对象才是真正的线程对象。
- 依然是通过调用线程对象的 start() 来启动线程。
public class MyThread implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread myThread=new MyThread();
Thread thread=new Thread(myThread);
thread().start();
//或者new Thread(new MyThread()).start();
}
}
3️⃣------------------------使用Callable和Future创建线程---------------------
不同于 Runnable 接口,Callable 接口提供了一个 call() 为线程的执行体,call() 比 run() 功能要强大:
- call() 可以有返回值;
- call() 可以声明抛出异常。
Java5 提供了 Future 接口来代表 Callable 接口里 call() 的返回值,并且为 Future 接口提供了一个实现类 FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务:
boolean cancel(boolean mayInterruptIfRunning)
:试图取消该 Future 里面关联的 Callable 任务。get()
:返回 Callable 里 call() 的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。get(long timeout,TimeUnit unit)
:返回 Callable 里 call() 的返回值,最多阻塞 timeout 时间,经过指定时间没有返回抛出 TimeoutException。boolean isDone()
:若 Callable 任务完成,返回 true。boolean isCancelled()
:如果在 Callable 任务正常完成前被取消,返回 true。
创建并启动有返回值的线程的步骤如下:
- 创建 Callable 接口的实现类,并实现 call(),然后创建该实现类的实例(从 Java8 开始可以直接使用 Lambda 表达式创建 Callable 对象)。
- 使用 FutureTask 类来包装 Callable 对象及 call() 的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程(因为 FutureTask 实现了 Runnable 接口)。
- 调用 FutureTask 对象的 get() 来获得子线程执行结束后的返回值。
public class Main {
public static void main(String[] args){
//使用Lambda表达式创建Callable对象
//使用FutureTask类来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(task,"有返回值的线程").start();
//实质上还是以Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:"+task.get());
//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
4️⃣----------使用ExecutorService、Callable、Future实现有返回结果的线程--------
ExecutorService、Callable、Future 三个接口实际上都是属于 Executor 框架。返回结果的线程是在 JDK1.5 中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。有返回值的任务必须实现 Callable 接口。类似的,无返回值的任务必须实现 Runnable 接口。
执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object。
注意:get()是阻塞的,线程无返回结果,get()会一直等待。
再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。如下是一个完整的有返回结果的多线程例子:
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
//有返回值的线程
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
long start = System.currentTimeMillis();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
long end = System.currentTimeMillis();
System.out.println("----程序结束运行----,程序运行时间【" + (end - start) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
@Override
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + (end - start) + "毫秒】";
}
}
二、对比
Runnable 接口中的 run() 的返回值是 void,它做的事情只是纯粹地去执行 run() 中的代码而已;Callable 接口中的 call() 是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了,某条线程执行了多久,某条线程执行时期望的数据是否已经赋值完毕。无法得知,能做的只是等待这条多线程的任务执行完毕而已。而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。
实现 Runnable 接口和实现 Callable 接口的方式基本相同。可以把这两种方式归为一种,这种方式与继承 Thread 类的方法之间的差别如下:
- 线程只是实现 Runnable 接口或实现 Callable 接口,还可以继承其他类。
- 这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。
- 编程稍微复杂。如果需要访问当前线程,必须调用 Thread.currentThread()。
- 继承 Thread 类的线程类不能再继承其他父类( Java 单继承)。
注:一般推荐采用实现接口的方式来创建多线程
对于线程的生命周期,一tu
三、线程池的使用
基础知识
1、Executors创建线程池
Java中创建线程池很简单,只需要调用Executors
中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads)
,但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Executors
创建线程池便捷方法列表:
方法名 | 功能 |
---|---|
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor
的构造方法。没错,上述Executors
方法创建的线程池就是ThreadPoolExecutor
。
2、ThreadPoolExecutor构造方法
Executors
中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor
的构造方法(定时任务使用的是ScheduledThreadPoolExecutor
),该类构造方法参数列表如下:
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
// 超过这个时间,多余的线程会被回收。
BlockingQueue<Runnable> workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSize
, maximumPoolSize
, workQueue
以及handler
:
corePoolSize
和maximumPoolSize
设置不当会影响效率,甚至耗尽线程;workQueue
设置不当容易导致OOM;handler
设置不当会导致提交任务时抛出异常。
正确的参数设置方式会在下文给出。
3、线程池的工作顺序
If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
Runnable和Callable
可以向线程池提交的任务有两种:Runnable
和Callable
,二者的区别如下:
- 方法签名不同,
void Runnable.run()
,V Callable.call() throws Exception
- 是否允许有返回值,
Callable
允许有返回值 - 是否允许抛出异常,
Callable
允许抛出异常。
Callable
是JDK1.5时加入的接口,作为Runnable
的一种补充,允许有返回值,允许抛出异常。
4、三种提交任务的方式:
提交方式 | 是否关心返回结果 |
---|---|
Future<T> submit(Callable<T> task) | 是 |
void execute(Runnable command) | 否 |
Future<?> submit(Runnable task) | 否,虽然返回Future,但是其get()方法总是返回null |
四、如何正确使用线程池
避免使用无界队列
不要使用Executors.newXXXThreadPool()
快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor
的构造方法手动指定队列的最大长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的行为
任务队列总有占满的时候,这是再submit()
提交新的任务会怎么样呢?RejectedExecutionHandler
接口为我们提供了控制方式,接口定义如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
线程池给我们提供了几种常见的拒绝策略:
拒绝策略 | 拒绝行为 |
---|---|
AbortPolicy | 抛出RejectedExecutionException |
DiscardPolicy | 什么也不做,直接忽略 |
DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
CallerRunsPolicy | 直接由提交任务者执行这个任务 |
线程池默认的拒绝行为是AbortPolicy
,也就是抛出RejectedExecutionHandler
异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy
,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512),
new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future
中,并在调用Future.get()
方法时获取,执行过程中的异常会被包装成ExecutionException
,submit()
方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
}
});
try {
Object result = future.get();
} catch (InterruptedException e) {
// interrupt
} catch (ExecutionException e) {
// exception in Callable.call()
e.printStackTrace();
}
上述代码输出类似如下:
线程池的常用场景
正确构造线程池
int poolSize = Runtime.getRuntime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,
0, TimeUnit.SECONDS,
queue,
policy);
获取单个结果
过submit()
向线程池提交任务后会返回一个Future
,调用V Future.get()
方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)
方法可以指定等待的超时时间。
获取多个结果
如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()
获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()
方法总是阻塞等待某一个任务完成,然后返回该任务的Future
对象。向CompletionService
批量提交任务后,只需调用相同次数的CompletionService.take()
方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:
void solve(Executor executor, Collection<Callable<Result>> solvers)
throws InterruptedException, ExecutionException {
CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
for (Callable<Result> s : solvers)// 提交所有任务
ecs.submit(s);
int n = solvers.size();
for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
Result r = ecs.take().get();
if (r != null)
use(r);
}
}
单个任务的超时时间
V Future.get(long timeout, TimeUnit unit)
方法可以指定等待的超时时间,超时未完成会抛出TimeoutException
。
多个任务的超时时间
等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:
public void testLatch(ExecutorService executorService, List<Runnable> tasks)
throws InterruptedException{
CountDownLatch latch = new CountDownLatch(tasks.size());
for(Runnable r : tasks){
executorService.submit(new Runnable() {
@Override
public void run() {
try{
r.run();
}finally {
latch.countDown();// countDown
}
}
});
}
latch.await(10, TimeUnit.SECONDS); // 指定超时时间
}
结合异常和等待的标准写法
public Resp getDatasourceListByOu(String ouId) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
String Path1 = "url1";
String Path2 = "url2";
String Path3 = "url3";
String Path4 = "url4";
List<String> urlList = new ArrayList<>();
if (!"".equals(Path1)) {
urlList.add(Path1);
}
if (!"".equals(Path2)) {
urlList.add(Path2);
}
if (!"".equals(Path3)) {
urlList.add(Path3);
}
if (!"".equals(Path4)) {
urlList.add(Path4);
}
List<FutureTask<String>> taskList = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(urlList.size());
for (String uri : urlList) {
FutureTask<String> futureTask = new FutureTask<>(new GetDataSourceListTask(ouId, uri, latch));
taskList.add(futureTask);
executorService.submit(futureTask);
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
String typeResult = "";
List<String> resultList = new ArrayList<>();
Resp resp = new Resp();
for (FutureTask<String> task : taskList) {
try {
typeResult = task.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
resp = JSON.parseObject(typeResult, resp.class);
List<String> typeList = resp.getData();
for (String typeNum : typeList) {
resultList.add(typeNum);
}
}
if (resp.getCode() == 200) {
return Resp.success(resultList);
} else {
return Resp.error(queryDataSourceTypeResp.getCode());
}
}
public class GetDataSourceListTask implements Callable<String> {
private final String ouId;
private final String url;
private final CountDownLatch latch;
public GetDataSourceListTask(String ouId, String url, CountDownLatch latch) {
this.ouId = ouId;
this.url = url;
this.latch = latch;
}
@Override
public String call() throws Exception {
Map<String,String> map = new HashMap<>();
map.put("ouId",ouId);
String s = HttpClientUtil.doGet(url, map);
latch.countDown();
return s;
}
}
线程池和装修公司
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 正式工数量
int maximumPoolSize, // 工人数量上限,包括正式工和临时工
long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
BlockingQueue<Runnable> workQueue, // 排期队列
ThreadFactory threadFactory, // 招人渠道
RejectedExecutionHandler handler) // 拒单方式
总结
Executors
为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor
的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService
提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch
完成。
五、springBoot的线程池管理
springboot的线程池配置
创建一个配置类ExecutorConfig,用来定义如何创建一个ThreadPoolTaskExecutor,要使用@Configuration和@EnableAsync这两个注解,表示这是个配置类,并且是线程池的配置类,如下所示:
@Configuration
@EnableAsync
public class ExecutorConfig {
private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Bean
public Executor asyncServiceExecutor() {
logger.info("start asyncServiceExecutor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(5);
//配置最大线程数
executor.setMaxPoolSize(5);
//配置队列大小
executor.setQueueCapacity(99999);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("async-service-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
}
一般搭配注解来实现,非常便捷
API性能优化之异步_人工智的博客-CSDN博客_异步api
数据库线程池我们可以使用阿里的druid,非常强大,监控也是非完备
MyBatis集成Druid实现数据库线程池管理(一)_java界的小学生-CSDN博客_mybatis集成druid
线程池关键上线文传递
线程本地变量是实现无锁编程的关键:首先要了解ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal这三个实现。这个不是本文的重点后续会专门梳理
ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之间区别_mztBang的博客-CSDN博客
Java 多线程上下文传递在复杂场景下的实践_vivo互联网技术-CSDN博客_多线程上下文传递
关键:线程池参数设置
IO密集型
由于 IO 密集型任务的 CPU 使用率较低,导致线程空余时间很多,所以通常就需要开 CPU
最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数经过简单的换算,以上公式可进一步转换为:最佳线程数目 = (线程等待时间与线程 CPU 时间之比 + 1 ) * CPU 核数通过公式可以看出:等待时间所占比例越高,需要越多线程; CPU 耗时所占比例越高,需要越少线程。下面举个例子:比如在 Web 服务器处理 HTTP 请求时,假设平均线程 CPU 运行时间为 100ms ,而线程等待时间(比如包括 DB 操作、 RPC 操作、缓存操作等)为 900ms ,如果 CPU核数为 8 ,那么根据上面这个公式,估算如下:( 900ms+100ms ) /100ms*8= 10*8 = 80