概述
本博客java云同桌学习系列,旨在记录本人学习java的过程,并与大家分享,对于想学习java的同学,我希望这个系列能够鼓励大家一同与我学习java,成为“云同桌”。
每月预计保持更新数量三章起,每章都会从整体框架入手,介绍章节所涉及的重要知识点及相关练习题,并会设置推荐学习时间,每篇博客涉及到的点都会在开篇目录进行总览。(博客中所有高亮部分(即双等号部分)表示是面试题进阶考点,或博主在求职过程中遇到的真题考点)
这里写目录标题
- 1.多线程概述
- 2.实现多线程的方式
- ==方式1:继承Thread==
- ==方式2:实现Runnable接口==
- 方式3:实现Callable<V>接口
- 3.Thread与ThreadLocal
- java.lang.Thread
- java.lang.ThreadLocal
- 4.实现线程中断
- 5.守护线程
- 6.线程安全相关问题及实现方式
- 线程安全方式一:同步代码块
- 线程安全方式二:同步方法
- 线程安全方式三:显式锁
- ==面试考点:公平锁与非公平锁==
- ==面试考点:隐式锁和显式锁的区别==
- ==面试考点:线程死锁==
- 7.多线程通信——生产者消费者模型
- 8.线程的状态
- 9.线程池技术
- 缓存线程池
- 定长线程池
- 单线程线程池
- 周期性任务定长线程池
- 自定义线程池
- ForkJoinPool 线程池
- 10.Lambda表达式
- 11.volatile关键字
- 12.CompletableFuture
- 12.1 执行方法
- 12.2 结果及异常处理方法
- 12.3 获取结果方法
- 12.4 异步串行任务方法
- 12.5 异步并行处理方法
学习时间:一周
学习建议:如果向着javaEE后端发展,多线程技术必定是离不开的,大量的并发问题等等需要多线程相关的知识,希望坚持到这里的小伙伴静下心来不要浮躁,继续学习,加油~
1.多线程概述
在正式学习之前,我们必须要弄清楚几组名词的含义
-
线程与进程
抽象类比:
进程:一个车间
线程:一个车间的一个工人进程
:程序执行过程中分配和管理资源的基本单位,是一个动态概念,拥有独立的内存空间
线程
:cpu任务调度和执行的基本单位,是进程的一个执行路径,与其他线程共享一个内存空间,栈空间独立 -
同步和异步
同步
:排队执行,只有前一个执行完毕才能轮到下一个执行,安全,但效率低
异步
:同时执行,多个任务同时对一个资源进行操作,无需等待,不安全,但效率高 -
并发与并行
并发
:不同任务在同一时间段内交替执行
并行
:不用任务在同一时间内同时执行 -
线程阻塞和线程中断
线程阻塞
:线程在等待某个耗时操作的完成,完成后才能继续线程接下来的任务
线程中断
:在java中,线程中断是线程的一个标志,线程中断表示该线程的中断标志设为了true,会产生InterruptedException,由程序员捕获到异常后自行选择处理方式
那么,何为多线程技术,即实现多个线程同时或交替运行,是开发高并发系统的基础
2.实现多线程的方式
方式1:继承Thread
1.可以自己编写一个线程类,使这个类继承于Thread类,
2.重写父类Thread中的run()方法,run()内容即为新线程执行的内容
3.在主线程中调用这个类对象的start()方法即可开启子线程执行run()方法里的内容
public class Test {
public static class Demo extends Thread {
@Override
public void run() {
//该新的线程执行的任务,只能通过Thread子类对象的start()方法调用
System.out.println("继承Thread方式线程已开启");
super.run();
}
}
public static void main(String[] args) throws IOException {
Demo demo = new Demo();
demo.start();//继承Thread方式线程已开启
}
}
方式2:实现Runnable接口
1.自己写一个类,实现Runnable接口,并重写run()方法
2.主线程里新建一个Thread线程对象,将Runnable实现类对象传递给构造方法
3.调用Thread线程对象的start()方法开启线程执行Runnable实现类run()方法的任务
public class Test {
public static class Demo implements Runnable {
@Override
public void run() {
//该新的线程执行的任务,只能通过Thread子类对象的start()方法调用
System.out.println("实现Runnable方式线程已开启");
}
}
public static void main(String[] args) throws IOException {
//创建一个任务对象
Demo demo = new Demo();
//创建一个线程,并分配任务
Thread thread = new Thread(demo);
//开启线程执行任务
thread.start();//实现Runnable方式线程已开启
}
}
开发中,较为常用的是方式2:实现Runnable接口,相比于方式1,具有以下优势
1.通过创建任务给线程分配的方式,适用于多个线程执行同一任务的情况
2.对于自建的类,可以避免单继承带来的局限性
3.任务与线程相分离,提高程序健壮性
4.在后续的线程池技术中,接受实现Runnable接口方式的任务,而不接受继承Thread类的线程
方式3:实现Callable<V>接口
方式3比较适用于主线程需要子线程处理一些数据然后将数据返回至主线程的情况,不是很常用。
1.自己写一个类,实现Callable接口,并重写里面的call()方法,传入的泛型即你需要子线程返回的数据类型
2.新建一个Futuretask对象,将Callable实现类对象作为构造参数
3.以Futuretask对象建立线程并开启
4.执行Futuretask对象的get()方法,使主线程一直等待call()方法执行完毕取得返回值
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
//2.以Callable实现类对象作为构造参数,创建Runnable子类Futuretask类对象
FutureTask futureTask = new FutureTask(myCallable);
//3.以Futuretask对象建立线程并开启
new Thread(futureTask).start();
//4.运行Futuretask对象的get()方法,是主线程一直等待直到子线程的call()方法执行完毕并返回值
System.out.println(futureTask.get());//100
}
//1.写一个Callable接口的实现类,泛型传入需要返回的数据类型
public static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 100;
}
}
}
Callable与Runnable相比
-
Runnable没有返回值;Callable可以返回执行结果
-
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
-
接口的定义不同
//Callable接口 public interface Callable<V> { V call() throws Exception; } //Runnable接口 public interface Runnable { public abstract void run(); }
3.Thread与ThreadLocal
java.lang.Thread
介绍:负责线程控制的类
构造方法:
- Thread()
- Thread(Runnable target)
- Thread(Runnable target, String name)
- Thread(String name)
此构造方法的重载的含义基本一致,如果指定了线程任务,则执行Runnable实现类的run()方法,若没有指定线程任务,则执行Thread子类的run()方法,线程名称未指定则为默认
常用方法:
- (static)currentThread()
返回当前正在执行的线程对象 - getName()
返回此线程的名称
public static void main(String[] args) throws IOException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},"这是个线程");
thread.start();
System.out.println(Thread.currentThread().getName());
/*输出:
main
这是个线程
*/
}
-
(static)sleep(long millis)
使当前线程休眠millis毫秒 -
start()
开启当前线程(JVM调用此线程的run()方法) -
setName(String name)
设置当前的线程名称 -
setDaemon(boolean on)
将此线程设置成为守护线程
java.lang.ThreadLocal
介绍:服务于每个单独的线程
ThreadLocal其核心在于其内部类ThreadLocal.ThreadLocalMap,其本质是一个entry,使用ThreadLocal作为key,自定义值Value作为值,即<ThreadLocal,value>,ThreadLocal提供了get/set方法,都是对此entry的操作
那么它是如何与每一个线程Thread挂钩呢?
答:原因在Thread类有一些属性,比如threadLocals属性,其类型就是上面提到的ThreadLocal.ThreadLocalMap,使其可以存放一些线程需要的值
但是,这样的机制会引发一个内存泄漏的问题,见下图
ThreadLocal作为了ThreadLocalMap的key值,那么当ThreadLocal结束引用,被GC回收后,ThreadLocalMap可能并没有结束生命周期,因为其作Thread的属性,生命周期跟随Thread,此时key不存在,而值还在,无法根据key取出里面的值,但依然占着内存,即造成了内存泄漏。
4.实现线程中断
在java中,线程中断可以理解为线程的一个标志位属性,通过其他线程调用该线程对象的interrupt()方法实现,表示该线程是否被其他线程设置为了中断状态。对于中断状态的处理,需要程序员捕获到因中断状态变为true而允许产生的InterruptedException后,自行决定在释放资源后终止线程。
产生中断并进行处理的步骤:
1.调用线程对象的interrupt()将该线程中断标志设为true
2.在该线程任务run()中,满足异常生成条件,生成InterruptedException异常
3.在线程任务run()中,捕获异常,自定义此次中断的处理方式(如需终止线程,直接return关闭run()方法)
以下是产生InterruptedException异常的情况(只有中断标志为true,以下产生条件才有效)
java.lang.Object#wait()
java.lang.Object#wait(long)
java.lang.Object#wait(long, int)
java.lang.Thread#sleep(long)
java.lang.Thread#interrupt()
java.lang.Thread#interrupted()
线程中断应用实例:
public static void main(String[] args) throws IOException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("开启线程");
//产生中断第二步:满足异常生成条件,生成异常
Thread.sleep(1000);
}catch (InterruptedException e){
//产生中断第三步:自定义中断处理方式
System.out.println("线程中断");
//直接return结束run方法相当于终止线程
return;
}
}
});
thread.start();
//产生中断第一步:中断标志设为true
thread.interrupt();
/*输出:
开启线程
线程中断
*/
}
5.守护线程
线程可以简单分为守护线程和用户线程
用户线程
:类比为:主人,直接建立的正常线程,可以执行所有任务
守护线程
:类比为:保姆,运行在后台。为用户线程提供一个额外的服务,生命周期依赖于用户线程,用户线程结束后,守护线程结束。
守护线程的实现:
1.需要在线程开启即start()前调用线程对象的setDaemon(true)将线程守护标志设为true;
2.守护线程中建立的新线程也是守护线程
3.守护线程中不可以执行持久化任务,例如文件的读写,因为守护线程随时可以停止,容易造成资源未正常释放
public class Test {
public static void main(String[] args) throws IOException, InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//子线程是否是守护线程
System.out.println("" +Thread.currentThread().isDaemon());
}
});
//子线程设置为守护线程
thread.setDaemon(true);
//开启子线程
thread.start();
//休息一会,防止守护线程没来得及输出就因主线程结束而结束
Thread.sleep(1000);
//主线程是否是守护线程
System.out.println("" +Thread.currentThread().isDaemon());
/*输出
true
false
*/
}
}
6.线程安全相关问题及实现方式
在之前的学习中,我们经常说一个类,是或不是线程安全的,这取决于这个类在线程执行时是同步还是异步,同步则线程安全,异步则线程不安全。
举个例子:如果需要线程一进入了一个if语句,在语句里正在执行其他语句更改数据,此时,线程二也进入了这个if语句,也在执行其他语句更改数据,本来按照逻辑,线程一更改完数据后,线程二是不可能进入这个if语句的,这时可能就会产生异常的数据。
实例:(创建三个线程,允许都对同一数据count同一时间段进行操作)
//线程不安全
public class Test {
public static void main(String[] args) {
Task task = new Task();
//创建多个线程执行这个任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
//创建一个任务
public static class Task implements Runnable{
int count = 10;
@Override
public void run() {
while(true){
if(count > 0){
System.out.println(Thread.currentThread().getName()+"准备");
try {
故意延长操作时间,使效果容易被观察到
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"操作为:"+count);
}else{
break;
}
}
}
}
}
/*输出:
Thread-2准备
Thread-1准备
Thread-0准备
Thread-0操作为:9
Thread-0准备
Thread-1操作为:8
Thread-1准备
Thread-2操作为:7
Thread-2准备
Thread-2操作为:6
Thread-1操作为:5
Thread-0操作为:4
Thread-1准备
Thread-2准备
Thread-0准备
Thread-2操作为:3
Thread-0操作为:2
Thread-0准备
Thread-1操作为:3
Thread-2准备
Thread-1准备
Thread-0操作为:1
Thread-2操作为:0
Thread-1操作为:-1
Process finished with exit code 0
*/
观看输出结果可以发现,本来限制条件是count>0,但现在出现了异常数据-1,原因是当count为1时,三个线程先后都在run()方法里,导致执行count–,出现了结果-1
线程安全
:在多线程访问同一数据时,确保运行结果与预期一致,而不会产生异常数据
线程安全方式一:同步代码块
利用synchronized关键字的同步代码块的应用方式,代表此代码块,只能被线程同步访问锁对象,任何对象都可以被打上同步锁的标记,在进入同步代码块时,必须需要先抢占锁对象。没有取得锁对象的线程只能等待下一次锁对象释放。一般的,我们把当前并发访问的共同资源作为同步锁对象.
格式:
synchronized(同步锁对象)
{
需要同步操作的代码
}
实例:(给操作count上锁,同一时间,只允许一个线程进入并操作count)
//线程安全:同步代码块
public class Test {
public static void main(String[] args) {
Task task = new Task();
//创建多个线程执行这个任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
//创建一个任务
public static class Task implements Runnable{
int count = 10;
//锁对象
Object object = new Object();
@Override
public void run() {
while(true){
synchronized (object){
if(count > 0){
System.out.println(Thread.currentThread().getName()+"准备");
try {
//故意延长操作时间,使效果容易被观察到
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"操作为:"+count);
}else{
break;
}
}
}
}
}
}
/*输出:
Thread-0准备
Thread-0操作为:9
Thread-0准备
Thread-0操作为:8
Thread-1准备
Thread-1操作为:7
Thread-1准备
Thread-1操作为:6
Thread-1准备
Thread-1操作为:5
Thread-2准备
Thread-2操作为:4
Thread-1准备
Thread-1操作为:3
Thread-1准备
Thread-1操作为:2
Thread-1准备
Thread-1操作为:1
Thread-0准备
Thread-0操作为:0
Process finished with exit code 0
*/
注意锁对象和所操作的数据只能放到各线程共用的代码区,例,不可放到run()方法里,每个线程都有一个自己的run()方法,那样无法上锁
线程安全方式二:同步方法
synchronize也可以对静态方法和实例方法进行同步,代码当该方法被一个线程抢占后,其他线程只能等待该线程释放该方法后再进行抢占执行
格式:
synchronized public void doWork(){
需要同步操作的代码
}
实例:(还是之前的背景,这次改为在run()方法里循环调用同步方法)
//线程安全:同步方法
public class Test {
public static void main(String[] args) {
Task task = new Task();
//创建多个线程执行这个任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
//创建一个任务
public static class Task implements Runnable{
int count = 10;
@Override
public void run() {
while(true) {
if (!task()) {
break;
}
}
}
//同步方法
synchronized public boolean task(){
//非静态方法默认锁对象是this
//静态方法默认锁对象是类名.class
if(count > 0){
System.out.println(Thread.currentThread().getName()+"准备");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"操作为:"+count);
return true;
}else{
return false;
}
}
}
}
/*输出:
Thread-0准备
Thread-0操作为:9
Thread-0准备
Thread-0操作为:8
Thread-0准备
Thread-0操作为:7
Thread-0准备
Thread-0操作为:6
Thread-0准备
Thread-0操作为:5
Thread-0准备
Thread-0操作为:4
Thread-0准备
Thread-0操作为:3
Thread-2准备
Thread-2操作为:2
Thread-2准备
Thread-2操作为:1
Thread-1准备
Thread-1操作为:0
Process finished with exit code 0
*/
线程安全方式三:显式锁
上述所介绍的两种方式,都属于隐式锁,相比这种比较隐晦。而显式锁,很简单,就是创建Lock子类ReentrantLock的一个对象,然后在同步代码前后手动上锁解锁。以面向对象的思想来看,更推荐显示锁。
格式:
Lock l = new ReentrantLock();
l.lock();
需要同步的代码
l.unlock();
实例:(在任务类里创建Lock类锁对象,在同步前手动上锁,同步后手动解锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//线程安全三:显式锁
public class Test {
public static void main(String[] args) {
Task task = new Task();
//创建多个线程执行这个任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
//创建一个任务
public static class Task implements Runnable{
int count = 10;
//创建显式锁对象
Lock l = new ReentrantLock();
@Override
public void run() {
while(true){
//上锁
l.lock();
if(count > 0){
System.out.println(Thread.currentThread().getName()+"准备");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"操作为:"+count);
}else{
break;
}
//解锁
l.unlock();
}
}
}
}
/*输出:
Thread-0准备
Thread-0操作为:9
Thread-0准备
Thread-0操作为:8
Thread-1准备
Thread-1操作为:7
Thread-1准备
Thread-1操作为:6
Thread-2准备
Thread-2操作为:5
Thread-2准备
Thread-2操作为:4
Thread-0准备
Thread-0操作为:3
Thread-1准备
Thread-1操作为:2
Thread-2准备
Thread-2操作为:1
Thread-0准备
Thread-0操作为:0
*/
面试考点:公平锁与非公平锁
公平锁
:可以类比为排队,线程在等待锁释放的过程中,先到先得,按顺序将锁赋予一下线程
非公平锁
:可以理解为抢绣球,线程在等待锁释放的过程中,没有先到先得的理念,只要锁释放了,所有等待线程都有机会获得锁,一起争抢,抢到即获得锁。
上述三种上锁的方式,都是非公平锁
实现公平锁,可以通过ReentrantLock类的另一个构造方法
- ReentrantLock(boolean fair)
创建对象时传递一个公平锁的参数,若为true,则为公平锁,false则为非公平锁
面试考点:隐式锁和显式锁的区别
- 层面不同
隐式锁:sync是JVM层面的锁
显示锁:是API层面的锁
- 使用方式
隐式锁:不需要程序员手动上锁和解锁,直接对某一块整体上锁解锁
显示锁:需要程序员自己去划分同步代码然后确定上锁和解锁的时机
- 公平方面
隐式锁:只能默认设置为非公平锁
显示锁:可以通过构造参数选择是公平锁还是非公平锁
面试考点:线程死锁
线程死锁可以简单理解为:A线程占有D资源,还需要C资源才能结束,B线程占有C资源,还需要D资源才能结束,然后两个线程就陷入了无限期的等待中,互不妥协。
现实生活中的例子也很多。例如:公司要求应聘者有工作经验才能入职,应聘者说只有入职我才能有工作经验/狗头~
死锁的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
最常用的避免死锁的方式是:
-
尽量不要将锁互相嵌套,造成互相等待的情况,
-
在资源使用完毕后,最好立即释放锁。
-
如果事先知道需要加的锁,最好按照顺序加锁,等待前一个锁大概解除后再进行加锁
7.多线程通信——生产者消费者模型
多线程通信的机制:生产者和消费者模型问题,
1.消费者线程先休眠,生产者线程工作;
2.工作完毕后,将数据给到消费者进行消费;
3.消费者线程被唤醒,生产者线程休眠;
4.然后消费完毕后;消费者再次休眠,生产者线程被唤醒进行生产。。。
生产数据————生产者休眠,消费者唤醒————消费数据————消费者休眠,生产者唤醒————生产数据
实现方式:
根据目前我们所学的知识,可以使用Object类的wait()和notify()方法实现
java.lang.Object
返回值 | 方法名 | 描述 |
---|---|---|
void | wait() | 使当前线程进入等待 |
void | wait(long timeoutMillis) | 使当前线程进入等待,等待时间为timeoutMills,时间结束自动被唤醒 |
void | notify() | 唤醒此对象正在等待的随机的一个单个线程 |
void | notifyAll() | 唤醒此对象正在等待的所有线程 |
某个对象拥有生产者消费者两个线程,如果生产或消费数据过后,通常由各自的线程执行以下这段代码:
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
生产者消费者模型的优点:
1. 降低耦合度,生产者和消费者不直接进行通信
2. 可以解决生产和消费速度不一致的问题
8.线程的状态
从Thread类中的内部类Thread.state中可以看到,线程共有6种状态
状态 | 描述 |
---|---|
BLOCKED | 线程的线程状态被阻塞等待监视器锁定。 |
NEW | 尚未启动的线程的线程状态。 |
RUNNABLE | 可运行线程的线程状态。 |
TERMINATED | 终止线程的线程状态。 |
TIMED_WAITING | 具有指定等待时间的等待线程的线程状态。 |
WAITING | 等待线程的线程状态。 |
状态之间的关系详情见下图:
中间从上到下是一条正常的主线,线程实例化后进入NEW状态,开启线程后进入RUNNABLE运行状态,运行结束终止线程TERMINATED状态,而其中的非主线状态都是在RUNNABLE运行状态时发生改变最后又回到运行状态,改变条件如图。
9.线程池技术
因为在实际应用中,创建消耗线程可能往往会比线程正常工作所耗费的资源和时间更多,所以为了降低资源消耗,提高响应速度,便于管理线程,引入了线程池的技术。
线程池,可以理解为存储线程的集合。在没有任务的时候,集合中会保留一段时间已创建的线程,长时间空闲的线程会被销毁;有任务的时候,会把任务交给线程池中空闲的线程去执行,任务如果足够多的话,还会对线程池进行扩容,提高并发量。
下面将会列出常用的四种线程池,他们都是通过Executors的不同静态方法创建的实例
//缓冲线程池
ExecutorService service = Executors.newCachedThreadPool();
//定长线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//单线程线程池
ExecutorService service = Executors.newSingleThreadExecutor();
//周期性任务定长线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
缓存线程池
这种线程池是最基础的线程池
特点:长度无限制
执行流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在,则创建线程 并放入线程池, 然后使用
实例:
public class Test {
public static void main(String[] args) {
//建立缓冲线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//建立任务对象
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
executorService.execute(myRunnable);
executorService.execute(myRunnable);
/*输出:
pool-1-thread-2正在执行
pool-1-thread-1正在执行
pool-1-thread-3正在执行
*/
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
//看一下当前是线程池中哪个线程在执行任务
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
}
定长线程池
这种线程池不可以扩容,在创建时便指定了固定长度
特点:长度固定,不可扩容
执行流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
实例:
public class Test {
public static void main(String[] args) {
//建立定长线程池,指定最大线程数为2
ExecutorService executorService = Executors.newFixedThreadPool(2);
//建立任务对象
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
executorService.execute(myRunnable);
executorService.execute(myRunnable);
executorService.execute(myRunnable);
/*输出:
pool-1-thread-2正在执行
pool-1-thread-2正在执行
pool-1-thread-1正在执行
pool-1-thread-2正在执行
*/
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
//看一下当前是线程池中哪个线程在执行任务
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
}
单线程线程池
单线程线程池只有一个线程在池,且不能扩容,效果等同于定长为1的定长线程池
特点:单线程
执行流程:
1. 判断线程池 的那个线程 是否空闲
2. 空闲则使用
3. 不空闲,则等待 池中的单个线程空闲后 使用
实例:
public class Test {
public static void main(String[] args) {
//建立单线程线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//建立任务对象
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
executorService.execute(myRunnable);
executorService.execute(myRunnable);
executorService.execute(myRunnable);
/*输出:
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-1正在执行
*/
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
//看一下当前是线程池中哪个线程在执行任务
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
}
周期性任务定长线程池
可以定时执行或周期定时执行任务的特殊线程池
特点:可以设置定时执行和定时周期执行
执行流程:
1.判断时间是否到达触发时间
2.判断线程池是否存在空闲线程
3.存在则使用
4.不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
5.不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
实例:
public class Test {
public static void main(String[] args) {
//建立周期定时定长线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//建立任务对象
MyRunnable myRunnable = new MyRunnable();
//定时执行(5秒后执行)参数分别为:任务对象,开始时间,时间单位
scheduledExecutorService.schedule(myRunnable,5, TimeUnit.SECONDS);
//定时周期执行(5秒后执行,每1秒再执行一遍)参数分别为:任务对象,开始时间,周期间隔,时间单位
scheduledExecutorService.scheduleAtFixedRate(myRunnable,5,1,TimeUnit.SECONDS);
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
//看一下当前是线程池中哪个线程在执行任务
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
}
自定义线程池
除了上述的几种常见的线程池,也提供了自定义线程池的方法
可以自定义以下七种参数:
核心线程大小、核心线程大小、存活时间、存活时间单位、任务队列、线程工厂、拒绝策略
示例代码:
private static ExecutorService executorService = new ThreadPoolExecutor(
//1.核心线程大小
//即使任务空闲,核心线程也不会被销毁
20,
//2.核心线程大小
//任务提交到线程池后,先进入任务队列,任务队列满后,若未达到最大线程数量,则新建一个线程执行任务
120,
//3.存活时间
//核心线程数量满后,若空闲时间>存活时间,则会被销毁,时间单位由下一个参数unit指定
30,
//4.存活时间单位
TimeUnit.SECONDS,
//5.任务队列
//5.1 定长阻塞队列,若队列已满且达到最大线程数量,则拒绝
new ArrayBlockingQueue<>(1),
//5.2 无界阻塞队列,不设置初始容量,则为Integer.MAX_VALUE,设置初始容量,则为定长队列
// new LinkedBlockingQueue<>()
//5.3 配对阻塞队列,相同于空容量队列,进入的任务必须等待消费线程消费后才能,继续接收任务,即生产——消费 配对后才能继续下一对
// new SynchronousQueue<>()
//5.4 可自定义Comparator比较器任务优先级的队列
// new PriorityBlockingQueue<>()
//6. 线程工厂,可利用ThreadFactoryBuilder().build()建立,可设置线程名,是否守护线程等
new ThreadFactoryBuilder().setNameFormat("ThreadName #%d").build(),
//7. 拒绝策略 当任务队列与最大线程数量已满时执行的处理
//7.1 拒绝任务,抛出创建线程池的主线程执行
new ThreadPoolExecutor.CallerRunsPolicy()
//7.2 抛弃任务,抛出RejectedExecutionException异常
// new ThreadPoolExecutor.AbortPolicy
//7.3 直接抛弃任务
// new ThreadPoolExecutor.DiscardPolicy
//7.3 抛弃最早进队列的任务,把这次拒绝任务加入队列
// new ThreadPoolExecutor.DiscardOldestPolicy
);
ForkJoinPool 线程池
该线程池在java源码中是最常用的默认线程池,主要核心是:计算时采用‘分治’算法,任务调度采用‘工作窃取’算法,也可称之为使用Fork/Join并行任务框架
分治算法
:类似于单机版的map-reduce计算,将大型任务拆分成若干小型任务(fork),至少无需再拆分,然后分别计算,然后整合结果(join)。
工作窃取算法
:当一个任务被拆分成若干小任务,负责该任务的线程内部会维护一个双端队列,放置需要计算的小任务,从队尾入队,队尾出队,即先进后出,当有空闲线程时,便会‘窃取’其他线程队头位置的小任务进行计算,先进先出。如此便能提高效率,防止部分线程在需要join时‘拖后腿’
10.Lambda表达式
为了降低代码冗余性,java提高了内部类的相关写法,但内部类一大坨看着也不够清晰明了;为了使代码更加清晰明了,java提高了匿名内部类的简写方式——Lambda表达式
如果有人进行过安卓开发,应该会非常熟悉,博主在进行Android开发时,使用Android Studio开发,IDE经常会自动将按钮的触发内部类的写法自动简写成Lambda表达式的方式。
其实假如我们编写一个匿名内部类,关注的重点不就是传递的参数和方法里执行的代码嘛,所以,Lambda只需要写形参和内部代码,就可以替代匿名内部类
格式:
() -> 内部代码
实例:(两种方式等价)
public class Test {
public static void main(String[] args) {
//建立单线程线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//非简写
executorService.execute(new Runnable() {
@Override
public void run() {
//看一下当前是线程池中哪个线程在执行任务
System.out.println(Thread.currentThread().getName()+"正在执行");
}
});
//Lambda表达式简写
executorService.execute(() -> System.out.println(Thread.currentThread().getName()+"正在执行"));
}
}
11.volatile关键字
当多个线程同时存在时,需要对同一个共享变量进行操作。它们会先从主内存中将该变量读取到自己工作内存中作为副本,操作结束后再更新会主内存。那么当其中一个线程更改了共享变量并更新到主内存时,依然存在一段时间,其余线程操作的还是之前获取到共享变量副本,没有及时更新。
Volatile
关键字的作用,即任意一个线程更改共享变量后,立即在主内存更新,并与其余线程同步更新。这种机制,也被称之为可见性
但是Volatile
并不是线程安全的,读写操作不上锁
故总结Volatile特点即:具有可见性,低成本,但线程不安全
12.CompletableFuture
Java 5 引入了Future接口,配合Callable可以创建异步线程执行任务,然后返回任务结果
Java 8 引入了CompletableFuture类,该类实现了Future接口,对于异步线程执行任务,获取任务结果,尤其是并行任务、多个异步任务依赖前一步结果的任务的处理,提供了更加便捷的方式
常用方法可以分为以下几类:
-
执行方法
-
结果及异常处理方法、
-
获取结果方法
-
异步串行处理方法
-
异步并行处理方法
12.1 执行方法
即基础的执行异步任务的方法
最常用的就是两种,分别对应有返回值与无返回值的异步任务
-
supplyAsync
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { System.out.println("begin"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return "finish"; }); String futureReturn = future.get(); System.out.println(futureReturn); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first begin * first finish * 执行耗时:3061 */ }
-
runAsync
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { System.out.println("begin"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 执行耗时:71 * begin */ }
上述两种执行方法都提供了自定义线程池参数的重载方法,默认不指定线程池使用的是ForkJoinPool线程池
12.2 结果及异常处理方法
对于异步任务执行过程中发生的异常的处理方法
-
exceptionally
此方法只会在执行时抛出异常后,才会执行内部逻辑,抛出的异常作为此方法的参数
此方法内部逻辑,可以捕获异常,然后改变返回结果
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); BigDecimal a = new BigDecimal(1); BigDecimal b = new BigDecimal(0); CompletableFuture<BigDecimal> future = CompletableFuture.supplyAsync( () -> a.divide(b) ); CompletableFuture<BigDecimal> exceptionally = future.exceptionally(throwable -> { System.out.println("只有发生异常时,才会进入此方法执行逻辑,发生异常" + throwable.getMessage()); return new BigDecimal(0); }); BigDecimal get = exceptionally.get(); System.out.println(get); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 只有发生异常时,才会进入此方法执行逻辑,发生异常java.lang.ArithmeticException: Division by zero * 0 * 执行耗时:49 */ }
更改一下a,b两个值,使其可以正常相除,就不会执行exceptionally内部逻辑了
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); BigDecimal a = new BigDecimal(2); BigDecimal b = new BigDecimal(1); CompletableFuture<BigDecimal> future = CompletableFuture.supplyAsync( () -> a.divide(b) ); CompletableFuture<BigDecimal> exceptionally = future.exceptionally(throwable -> { System.out.println("只有发生异常时,才会进入此方法执行逻辑,发生异常" + throwable.getMessage()); return new BigDecimal(0); }); BigDecimal get = exceptionally.get(); System.out.println(get); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 2 * 执行耗时:64 */ }
-
whenComplete
此方法可以在内部操作异常、操作执行结果,但不可以更改执行结果、更改异常,异常还是会正常抛出
此方法的参数,分别为执行结果、异常,如果发生异常,执行结果为null,如果未发生异常,异常参数为null
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); int a = 2; int b = 0; CompletableFuture<Integer> future = CompletableFuture.supplyAsync( () -> a/b ).whenComplete((result,throwable) -> { System.out.println("无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:" + (throwable != null ? throwable.getMessage() : null)); System.out.println("可在内部操作result结果,result为: " + result); }); Integer result = future.get(); System.out.println(result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:java.lang.ArithmeticException: / by zero * 可在内部操作result结果,result为: null * Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero * at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357) * at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895) * at com.example.demo.test.SynchronizedDemo.main(SynchronizedDemo.java:37) * Caused by: java.lang.ArithmeticException: / by zero * ...... */ }
更改一下a,b两个值,使其正常相除,可以看到异常参数为null
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); int a = 2; int b = 1; CompletableFuture<Integer> future = CompletableFuture.supplyAsync( () -> a/b ).whenComplete((result,throwable) -> { System.out.println("无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:" + (throwable != null ? throwable.getMessage() : null)); System.out.println("可在内部操作result结果,result为: " + result); }); Integer result = future.get(); System.out.println(result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:null * 可在内部操作result结果,result为: 2 * 2 * 执行耗时:56 */ }
-
handle
此方法相比whenComplete,多了一项更改执行执行结果的能力,甚至与更改执行结果的类型
相比exceptionally ,同样也能捕获处理异常,是功能最为强大的结果及异常处理方法
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); int a = 2; int b = 0; CompletableFuture<String> future = CompletableFuture.supplyAsync( () -> a/b ).handle((result,throwable) -> { System.out.println("无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:" + (throwable != null ? throwable.getMessage() : null)); System.out.println("可在内部操作result结果, result值为: " + result); return "finish"; }); String result = future.get(); System.out.println("最终执行结果类型" + result.getClass() + ",值为 " + result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:java.lang.ArithmeticException: / by zero * 可在内部操作result结果, result值为: null * 最终执行结果类型class java.lang.String,值为 finish * 执行耗时:59 */ }
更改a,b 值,使其正常相除,异常参数为null
public static void main(String[] args) throws Exception{ long begin = System.currentTimeMillis(); int a = 2; int b = 1; CompletableFuture<String> future = CompletableFuture.supplyAsync( () -> a/b ).handle((result,throwable) -> { System.out.println("无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:" + (throwable != null ? throwable.getMessage() : null)); System.out.println("可在内部操作result结果,result类型为" + result.getClass() + ", result值为: " + result); return "finish"; }); String result = future.get(); System.out.println("最终执行结果类型" + result.getClass() + ",值为 " + result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * 无论是否发生异常,都会执行,无异常时,throwable参数为null,异常为:null * 可在内部操作result结果,result类型为class java.lang.Integer, result值为: 2 * 最终执行结果类型class java.lang.String,值为finish * 执行耗时:77 */ }
可以对上述3个方法做个横向对比
exceptionally whenComplete handle 可捕获处理异常 不可捕获处理异常 可捕获处理异常 异常时执行 不论是否异常均执行 不论是否异常均执行 可获取到异常,不可获取到上一步执行结果 可获取到异常,可获取到上一步执行结果 可获取到异常,可获取到上一步执行结果 不可更改执行结果及类型 不可更改执行结果及类型 可更改执行结果及类型
12.3 获取结果方法
获取异步任务结果的方法主要就两种
-
get
-
get(long timeout, TimeUnit unit)
-
join
这两种的区别是,get方法获取会抛出非运行时异常,需要try–catch处理,join方法会抛出运行时异常
共性是,都是阻塞主线程,直到异步线程结果才能拿到结果,get的重载方法可以设置阻塞超时时间
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(
() -> 2/0
);
future.join();
future.get();//get方法由于会抛出非运行时异常,IDEA会报红线无法编译
12.4 异步串行任务方法
适用场景:某个异步任务执行完后,需要再做第二个任务,这样的场景也正是CompletableFuture的优点之一,无需让主线程等待第一个异步任务执行完后再手动启动第二个任务
-
thenRun
前后任务没有参数传递,后一个任务没有返回值
public static void main(String[] args){ long begin = System.currentTimeMillis(); CompletableFuture<Void> future = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("first end"); return "first end"; } ).thenRun(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("second end"); }); future.join(); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first end * second end * 执行耗时:2095 */ }
可以看到执行完thenRun之后,返回值泛型成了Void,从执行时间也可以看出是串行执行了第二个任务
-
thenAccept
接收前一个任务的参数,后一个任务没有返回值
public static void main(String[] args){ long begin = System.currentTimeMillis(); CompletableFuture<Void> future = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "first end"; } ).thenAccept((result) -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(result + " and second end"); }); future.join(); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first end and second end * 执行耗时:2093 */ }
可以看到,依旧没有返回值,但后一个任务可以从参数中拿到前一个任务结果
-
thenApply
接收前一个任务的参数,后一个任务可以有返回值
public static void main(String[] args){ long begin = System.currentTimeMillis(); CompletableFuture<String> future = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "first end"; } ).thenApply((result) -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return result + " and second end"; }); String result = future.join(); System.out.println(result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first end and second end * 执行耗时:2083 */ }
后一个任务不仅可以拿到前一个任务返回值,也可以设置自己的返回值
12.5 异步并行处理方法
同时并行执行多个异步任务,然后以or或and的关系统一处理
-
anyof
以不定长CompletableFuture作为参数,当任意一个异步任务执行完毕,则返回其对应的结果,返回值泛型为Object,如果任务一个任务抛出异常,anyof也会抛出异常
public static void main(String[] args){ long begin = System.currentTimeMillis(); CompletableFuture<String> first = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("first end"); return "first end"; } ); CompletableFuture<String> second = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("second end"); return "second end"; } ); Object result = CompletableFuture.anyOf(first, second).join(); System.out.println("执行结果:" + result); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first end * 执行结果:first end * 执行耗时:1071 */ }
可以看到,由于first任务先执行完,所以返回了first对应的返回值,然后主线程结束,jvm关闭,second任务使用的默认线程池ForkJoinPool生成的是守护线程,随着jvm关闭也中断,没有打印出对应字符串
同样,如果任务一个任务抛出异常,anyof也会抛出异常
-
allof
同样也已不定长CompletableFuture作为参数,当全部异步任务都执行完毕,才算作allof执行完毕,无返回值,如果任务一个任务抛出异常,allof也会抛出异常
public static void main(String[] args) { long begin = System.currentTimeMillis(); CompletableFuture<Integer> first = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("first end"); return 2/1; } ); CompletableFuture<String> second = CompletableFuture.supplyAsync( () -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("second end"); return "second end"; } ); CompletableFuture.allOf(first, second).join(); long end = System.currentTimeMillis(); System.out.println("执行耗时:" + String.valueOf(end-begin)); /**输出: * first end * second end * 执行耗时:2075 */ }
可以看到,first执行完后,还在等待second执行完毕,最后主线程耗时与最长的second任务的耗时接近
国庆假期加班不易,都学习到这里了,不妨点个赞加个关注呗~
最后
以上就是受伤小虾米为你收集整理的java云同桌学习系列(九)——多线程的全部内容,希望文章能够帮你解决java云同桌学习系列(九)——多线程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复