上一次更新多线程的内容还是在今年三月份,当时发布的内容不完整,仅介绍了Thread和Runnable两种创建多线程的方式,但是其实这两种方式在我的工作中是没有使用到的,当时项目正上线也没有特意去关注,所以这次来填坑。
这篇文章主要是想来补充一下上次没有介绍完全的内容,再学习一下线程池,以及在工作中用到的创建多线程的方式。
实现Callable接口方式创建多线程
Callable属于JUC——java.util.concurrent包下的创建多线程的方式
使用Callable方式来创建多线程的步骤分为:
1.实现Callable接口,设置返回值类型
2.重写call方法
3.创建目标对象 ——为实现Callable接口的类对象,需要创建几个线程,就要创建几个对象
4.创建执行服务 —— 创建几个线程,线程个数就为几
ExecutorService 自定义执行服务名称 = Executors.newFixedThreadPool(线程个数)
5.提交执行 —— 创建一个目标对象,就要写一次提交执行的代码
Future<Boolean> 多线程执行结果 = 执行服务名称.submit(目标对象)
6.获取结果 —— 每有一个提交执行,就要获取一个结果
boolean r1 = 多线程执行结果.get()
7.关闭服务
执行服务名称.shutdownNow()
这种创建多线程的方式比较复杂,但是好处在于可以设置返回值,可以抛出异常(其他两种创建多线程方式则不允许抛出异常)。
使用FutureTask对象创建多线程
1.我们需要创建一个Callable对象
Callable<返回值类型> callable = () -> {
子任务内容;
return 返回值;
}
2.然后创建一个FutureTask对象
FutureTask<返回值类型> task = new FutureTask(callable)
3.创建一个Thread类对象,将FutureTask对象作为参数,执行线程
Thread thread = new Thread(task)
//执行多线程
thread.start()
4.获取多线程返回值与异常
当我们调用了get方法后,主线程就会一直不停的询问子线程状态,是否执行结束,会等待子线程执行结束后才继续向下执行。
//线程正常执行完毕会正常返回,如果报错则会返回ExecutionException异常
//我们可以填写get()方法的参数,限定等待时长
//超过时长会抛出TimeoutException,我们可以捕获后处理此异常
task.get()
5.获取子线程产生的异常内容
我们通过第四步获取到了异常后,通过捕获ExecutionException异常,可以调用getCause()方法来获取子线程中产生的异常内容
CompletableFuture方式创建线程
CompletableFuture是Java8中提出的创建多线程的方式,也是我在工作中使用的创建多线程的方式
//查询当前电脑CPU有多少核心
Runtime.getRuntime().availableProcessors()
//设置JVM启动时的最大线程数
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "最大线程个数")
//查看当前线程数
ForkJoinPool.commonPool().getPoolSize()
//查看最大线程数
ForkJoinPool.getCommonPoolParallelism()
CompletableFuture创建线程的方式就比较简单
//通过runAsync方法执行的代码,会自动跳到另一个线程中执行(无返回值)
CompletableFuture<返回值类型> thread = CompletableFuture.runAsync(() -> {
方法体
return 返回值
});
//通过supplyAsync方法执行的代码,会自动跳到另一个线程中执行(有返回值)
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
});
//等待线程执行结束获取返回结果,此方法会抛出运行时异常,不需要强制捕获
thread.join();
//等待所有线程执行结束
CompletableFuture.allOf().join()
//通过thenCompose方法,当第一个线程结束后,再开启另一个线程
//线程2中可以使用线程1的返回值数据
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
}).thenCompose(threadReturn -> CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
}));
//通过thenCombine方法,当前两个线程执行结束后,在开启第三个线程
//线程3中可以使用线程1和线程2的返回值数据
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
方法体
return 返回值1
}).thenCombine(CompletableFuture.supplyAsync(() -> {
方法体
return 返回值2
}), (返回值1, 返回值2) -> {
方法体
return 返回值3
});
//通过applyToEither方法,线程1跟线程2一起执行,哪个先执行完就将哪个结果返回
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
}).applyToEither(threadReturn -> CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
}), 最终的返回值参数 -> {方法体(可以进行数据处理,然后返回)});
//如果直接返回则为 返回值参数 -> 返回值参数
//通过exceptionally方法,可以捕获线程出现的异常进行处理,后面可以接上其他线程操作
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
方法体
return 返回值
}).exceptionally(e -> {
异常处理
return 异常状态的返回值
});
守护线程
线程分为用户线程和守护线程。
守护线程是保护我们正常运行程序的线程,例如:gc 垃圾回收线程。
代码运行时虚拟机不必等待守护线程结束。
synchronized锁
synchronized 添加在方法作用域标识符后面,用来表示此方法为同步方法。
synchronized 默认锁的是this,如果不希望锁住this对象,则需要使用synchronized代码块实现
//synchronized 锁this例子
public synchronized 返回值类 方法名(){
}
//synchronized 锁对象的例子(锁上需要增删改查的对象)
synchronized (object) {
}
synchronized是隐式锁,出了作用域自动解锁。
synchronized锁比较笨重,JVM需要较长事件来进行线程的调度。
死锁
多个线程持有对方需要的资源互相还需要对方的资源,就会形成死锁。
像多线程、数据库层面容易发生死锁事件,我们应该去了解一些底层原理,明白代码是如何运行的,来避免死锁问题的发生。
sleep方法与wait方法的区别
这个区别在工作中不常用到,但是经常被用作面试的提问,所以在这里总结一下区别。
区别一:sleep是Thread类中的静态方法,在执行sleep方法时,不会释放此线程的锁,待睡眠结束后继续执行此线程;wait是Object类中的方法,再执行wait方法时,会释放此线程的锁,等待其他线程调用notify方法或是指定时间以后重新回到线程队列等待执行。
区别二:sleep常用于增强多线程测试时出现问题的可能性,不涉及多线程的互相通信;wait方法涉及多线程的互相通信。
区别三:sleep方法可以放在任何地方使用,而wait方法只能在同步方法或同步方法块中使用。
线程中断
其实这部分我不是很想说了,因为平时在工作中不太用到,但是觉得还是比较重要的吧,所以来说一下线程中断。
我们在调用sleep方法或wait方法时,总是需要抛出InterruptedException(线程中断异常),因为在我们休眠等待的时候可能其他线程会调用我们的中断方法,唤醒我们重新让我们执行。
中断状态是线程的一个状态,当线程已经为中断状态时不会自动恢复。
方法名 | 作用 |
interrupt() | 设置线程的中断状态 |
isInterrupt() | 判断线程是否为中断状态 |
interrupted() | 清除线程的中断状态 |
sleep方法也会清除线程的中断状态。
当我们调用sleep方法开始睡眠后,等待其他方法执行到interrupt方法的时候,就会自动唤醒调用方法的线程,继续向下执行。
线程池
现在接触到的池类概念一共有两个:线程池,数据库连接池。
先说一下这个设计的优点,为什么需要有池。
当我们创建线程,或是数据库连接的时候会十分消耗资源,所以使用池的概念可以将创建好的线程或数据库连接进行重复利用。使用时从池中拿出对象,使用完毕放回池中,避免频繁的创建销毁。
1.可以提高响应速度(避免了创建销毁浪费的时间)。
2.降低资源消耗。
3.便于对线程进行管理。
在阿里巴巴的编码规约中,建议我们创建线程池的时候通过ThreadPoolExecutor 的方式创建,这样的处理方式可以让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
那么该如何创建ThreadPoolExecutor线程池呢?
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //最大超时时间
TimeUnit unit, //超时时间单位
BlockingQueue<Runnable> workQueue, //缓冲阻塞队列,用于等待
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) //拒绝策略
在创建线程池时我们应该如何确定线程池的数量呢?
有两种策略:
1.CPU密集型,我们按照服务器的CPU核数来确定线程池中个数,这样可以保持CPU的效率最高。
// 获取CPU核数/处理器个数
Runtime.getRuntime().availableProcessors()
2.IO密集型,我们要自主判断程序中有多少个大型任务,大型任务十分耗费资源,我们可以根据大型任务的两倍数量来进行线程池的创建,这样可以避免程序进入阻塞状态。
拒绝策略参数一共有如下四种
//(默认拒绝策略)会抛出RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy()
//重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功
new ThreadPoolExecutor.CallerRunsPolicy()
//对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列
new ThreadPoolExecutor.DiscardOldestPolicy()
//对拒绝任务直接无声抛弃,没有异常信息
new ThreadPoolExecutor.DiscardPolicy()
当一个多线程方法被添加到线程池时,线程池的运行策略:
1.如果当前线程池中的线程数量小于corePoolSize,即使线程都是空闲状态,也要创建新的线程来处理添加的任务。
2.如果当前线程池中的线程数量大于等于corePoolSize,但缓冲阻塞队列未满,则任务会被放入阻塞队列中。
3.如果当前线程池中的线程数量大于corePoolSize,缓冲阻塞队列满了,且线程数量小于maximumPoolSize,则创建新的线程来处理添加的任务。
4.如果当前线程池中的线程数量大于corePoolSize,缓冲阻塞队列满了,且线程数量大于maximumPoolSize,则通过设置的拒绝策略来处理 添加的任务。