目录
- 进程与线程
- 进程
- 线程
- 几个基本概念
- 串行和并行
- 并行与并发
- 多线程
- 概念
- 多线程的优点/好处
- 多线程问题分析
- Java多线程的基本使用
- Thread类
- 主线程
- 守护线程
- 案例:显示主线程名
- 多线程实现方式
- 继承java.lang.Thread类
- 步骤
- 代码实现
- start()和run()的区别?
- 注意
- 实现Runnable接口创建线程
- 步骤
- 代码实现
- 实现Callable接口
- 步骤
- 代码实现
- 线程状态
- CPU执行原理
- 补充
- 线程调度
- 线程优先级
- 线程休眠|sleep()
- 线程的强制运行|join()
- 分析
- 线程的礼让|yield()
- 面试题:sleep()和yield()的区别
- 线程中断|interrupt()
- 线程等待|wait()
- wait()方法的主要功能
- wait()做的三件事
- 代码案例
- 线程唤醒
- notify()方法
- 代码实现
- notifyAll()方法
- 代码实现
- 面试题:wait和sleep的区别
- 多线程JVM内存分配
- 线程安全
- 案例分析
- 代码
- 变量对线程安全的影响
- 线程同步
- 与异步的区别
- 同步代码块
- 原理:
- 同步方法
- 如何解决线程安全问题
- lock方式
- 代码实现
- lock和同步块比较
- 总结
- 死锁
- 死锁的产生
- 案例代码
- 解决方案
- 代码示例
- 守护线程
- 守护线程的特点:
- 守护线程用在什么地方呢?
- 线程池
- 线程池的工作机制
- 线程池优点
- 线程池的构造方法
- 案例分析
- 线程池的状态
- 线程池的主要流程
- 拒绝策略
- 提交任务
- 各种线程池使用
- 1.单一线程池
- ExecutorService的submit和execute方法区别
- 2. 固定线程池
- 3. 可变线程池
- 4. 定时线程池(任务调度线程池)
- 线程池的关闭
- 四种线程池比较
- 5. SpringBoot中使用自定义线程池
- 配置
- 配置类
- 测试
- 运行结果
- 补充:线程数据传递
- 通过构造方法传递数据
- 通过变量和方法传递数据
- 通过回调函数传递数据
进程与线程
进程
- 应用程序的执行实例,有独立的内存空间和系统资源
- 当一个程序被运行,那么就相当于在这台电脑上开启了一个进程,比如我们可以看到任务管理器中的IDE,Firefox,WPS,微信等
- 程序由指令和数据组成;指令要运行,数据要加载;指令被CPU加载运行,数据被加载到内存;指令运行时可由CPU调用硬盘、网络等设备
线程
- CPU调度和分派的基本单位,进程中执行运算的最小单位,可完成一个独立的顺序控制流程
- 一个进程内可以分为多个线程
- 一个线程就是一个指令流,CPU调度的最小单位,由CPU一条一条的执行指令
几个基本概念
串行和并行
-
串行:相对于单条线程来执行多个任务来说,以下载文件为例,当下载多个文件时,一般理解和操作就是按照一定顺序进行下载,也就是说,有A,B,C三个文件时,必须等A下载完之后才能去下载B,然后B下载完才能下载C,他们在时间上不可能重叠
-
并行:下载多个文件时,开启多条线程,多个文件同时进行下载,这里是同一时刻发生的,并行在时间上是重叠的
并行与并发
- 并发:单核CPU运行多线程时,时间片进行很快的切换,多个线程轮流执行CPU
- 并行:多核CPU运行,多线程时,真正的在同一时刻运行
多线程
概念
- 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程”
- 多个线程交替占用CPU资源,而非真正的并行执行
多线程的优点/好处
- 充分利用CPU的资源
- 简化编程模型
- 带来良好的用户体验
多线程问题分析
- 先说单线程,从流程上来看,单线程只有一条之执行线,过程直观,很好理解代码的执行过程
- 多线程是多条执行线,企鹅一般多条执行线之间有所交互,多条执行线之间可能需要通信,因此会产生以下问题
- 多线程的执行结果不确定,因为受到CPU调度的影响
- 多线程引发的安全问题(如资源共享问题)
- 线程资源比较宝贵,依赖线程池操作线程,线程池的参数如何设置是个问题
- 多线程的执行是动态的,且几乎是同时的,难以追踪过程
- 多线程的底层是操作系统层面,源码难度大
Java多线程的基本使用
Thread类
- Java提供了java.lang.Thread类支持多线程编程
主线程
- main()方法即为主线程入口
- 产生其他子线程的线程
- 必须最后完成执行,因为它执行各种关闭动作
守护线程
- 也叫后台线程,如:jvm的垃圾回收线程、jvm的异常处理线程。
案例:显示主线程名
public static void main(String args[]) {
Thread t= Thread.currentThread();
System.out.println("当前线程是: "+t.getName());
t.setName("MyJavaThread");
System.out.println("当前线程名是: "+t.getName()); }
多线程实现方式
- 继承Thread类 (可以说是 将任务和线程合并在一起)
- 实现Runnable接口 (可以说是 将任务和线程分开了)
- 实现Callable接口 (利用FutureTask执行任务)
- 线程池
继承java.lang.Thread类
步骤
- 自定义一个类MyThread类,用来继承与Thread类
- 在MyThread类中重写run()方法
- 在测试类中创建MyThread类的对象
- 启动线程:start()
代码实现
线程类
public class MyThread01 extends Thread{
@Override
public void run(){
for (int i = 1; i <= 10; i++){
Thread.currentThread().setName("线程1:");
System.out.println(Thread.currentThread().getName() + i);
}
}
}
public class MyThread02 extends Thread{
@Override
public void run(){
for (int i = 1; i <= 10; i++){
Thread.currentThread().setName("线程2:");
System.out.println(Thread.currentThread().getName() + i);
}
}
}
启动线程
public class Main {
public static void main(String[] args) {
MyThread01 myThread01 = new MyThread01();
//myThread01.run();
MyThread02 myThread02 = new MyThread02();
myThread01.start();//启动线程
myThread02.start();//启动线程
for (int i = 1; i <= 10; i++){
System.out.println(Thread.currentThread().getName() + i);
}
}
}
start()和run()的区别?
- run()不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
- start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
- 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
- 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
- 单纯使用run()方法是不能多线程并发的。
注意
- 多个线程交替执行,不是真正的“并行”
- 线程每次执行时长由分配的CPU时间片长度决定
实现Runnable接口创建线程
步骤
- 自定义一个MyRunnable类来实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去
- 启动线程(start())
代码实现
线程类
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.currentThread().setName("线程1:");
System.out.println(Thread.currentThread().getName() + i);
}
}
}
public class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.currentThread().setName("线程2:");
System.out.println(Thread.currentThread().getName() + i);
}
}
}
启动线程:不会触发资源争夺
public class Main {
public static void main(String[] args) {
/**
* 这里针对两个对象分别创建了Thread线程对象
* 因此t1和t2两个线程不会触发资源争夺
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"线程1:");
MyThread2 myThread2 = new MyThread2();
Thread t2 = new Thread(myThread2,"线程2:");
t1.start();
t2.start();
}
}
启动线程:触发资源争夺
public class Test {
public static void main(String[] args) {
/**
* 这里针对一个对象分别创建了Thread两个线程对象
* 因此t1和t2两个线程会触发资源争夺(争夺的正式myThread这对象的资源)
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"线程1:");
Thread t2 = new Thread(myThread,"线程2:");
t1.start();
t2.start();
}
}
实现Callable接口
- java.util.concurrent.FutureTask; /JUC包下的,属于java的并发包
步骤
- 自定义一个MyCallable类来实现Callable接口
- 在MyCallable类中重写call()方法
- 创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。
- 启动线程
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
代码实现
public class MyCallable implements Callable<Integer>{
@Override
public Integer call(){
int i=0;
for(;i<5;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable t1=new MyCallable();
FutureTask task1=new FutureTask(t1);
Thread thread1=new Thread(task1,"T1");
thread1.start();
MyCallable t2=new MyCallable();
FutureTask task2=new FutureTask(t2);
Thread thread2=new Thread(task2,"T2");
thread2.start();
System.out.println(task1.get());
System.out.println(task2.get());
//注意:thread1和thread2最好分别执行两个task对象(两个task对象也分别采用两个Callable对象,否则只会有一个线程执行)
}
线程状态
-
新建状态
- 新创建了一个线程对象
-
就绪状态
- 就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。
- 当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
-
运行状态
- run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片
- 当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。
-
阻塞状态
- 当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。
- 之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。
- 阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
-
锁池
- 在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待
- 如果找到了会进入就绪状态继续抢夺CPU时间片。
- 这个进入锁池,可以理解为一种阻塞状态
-
死亡状态
- 线程执行完了或者因异常退出了run()方法,该线程结束生命周期
CPU执行原理
- 所有可运行的线程进入队列中等待执行。
- CPU每次会选择其中一个线程来执行。
- CPU会为本次执行该线程分配一个时间片。
- 如果时间片到期,CPU会将该线程放入队列中,该线程继续等待执行。
- 如果时间片没有到期,运行时出现阻塞,CPU会将该线程放入阻塞队列。
- 如果时间片没有到期,线程代码执行完毕,该线程被销毁。
补充
- CPU每次随机选择哪个线程来执行
- CPU每次随机分配的时间片大小
- 多线程程序每次执行结果都可能不一样
线程调度
- 线程调度指按照特定机制为多个线程分配CPU的使用权
方 法 | 说 明 |
---|---|
void setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程 |
boolean isAlive() | 测试线程是否处于活动状态 |
线程优先级
- 线程优先级由1~10表示,1最低,默认优先级为5
- 优先级高的线程获得CPU资源的概率较大
- 均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(),"线程A");
Thread t2 = new Thread(new MyThread(),"线程B");
t2.setPriority(Thread.MAX_PRIORITY);
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}}
线程休眠|sleep()
- 让线程暂时睡眠指定时长,线程进入阻塞状态
- 睡眠时间过后线程会再进入可运行状态
- millis为休眠时长,以毫秒为单位
- 调用sleep()方法需处理InterruptedException异常
线程类
public class People extends Thread{
private int time;
private String name;
private int km = 15;
public People(int time,String name){
this.time = time;
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= km; i++){
System.out.println(name + "爬完" + (i * 100) + "米");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
启动线程
public static void main(String[] args) {
People y = new People(500,"年轻人");
People o = new People(700,"老年人");
y.start();
o.start();
}
线程的强制运行|join()
- 使当前线程暂停执行,等待其他线程结束后再继续执行本线程
- millis:以毫秒为单位的等待时长
- nanos:要等待的附加纳秒时长
- 需处理InterruptedException异常
分析
- 在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,
- 但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
线程类
public class Vip extends Thread{
@Override
public void run() {
for (int i= 1;i <= 10;i++){
System.out.println("特需号:"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
启动线程
public static void main(String[] args) {
Vip vip = new Vip();
vip.start();
for (int i = 1; i <= 50; i++) {
System.out.println("普通号:"+i);
try {
Thread.sleep(500);
if(i == 10){
vip.join();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
线程的礼让|yield()
- 暂停当前线程,允许其他具有相同优先级的线程获得运行机会
- 该线程处于就绪状态,不转为阻塞状态
- 只是提供一种可能,但是不能保证一定会实现礼让
线程类
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
if(i == 3){
System.out.print(Thread.currentThread().getName() + "线程礼让:");
Thread.yield();
}
}
}
}
启动线程
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"线程A:");
Thread t2 = new Thread(myThread,"线程B:");
t1.start();
t2.start();
}
面试题:sleep()和yield()的区别
- sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
- sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
- 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
线程中断|interrupt()
- 不是中断某个线程,只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程
- 但是如果你捕获这个异常,那么这个线程还是不会中断的
线程等待|wait()
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程。置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
- wait()方法必须和synchronized搭配使用,在进入线程等待前,必须要获得这个锁对象的控制权,一般情况下是放到synchronized(obj)块当中去,如果不加锁,就会出现IllegalMonitorStateException异常。
wait()方法的主要功能
- 线程同步:wait()方法通常与synchronized关键字一起使用,用于实现线程的同步。当一个线程执行了对象的wait()方法后,它会释放对象的锁,并进入等待状态,直到其他线程通过notify()或notifyAll()方法来唤醒它并获取锁。
- 等待条件满足:线程可以调用wait()方法来等待某个特定条件的满足。当条件不满足时,线程可以通过wait()方法进入等待状态,直到条件满足后再继续执行。
- 防止资源浪费:wait()方法可以用于防止资源浪费。当线程需要等待某个事件的发生时,可以调用wait()方法进入等待状态,直到事件发生后再继续执行,这样可以避免线程的空轮询或忙等待,节省了系统资源。
wait()做的三件事
- 让当前线程阻塞等待(把这个线程的PCB从就绪队列中放到阻塞队列中),准备接受通知
- 释放当前锁(要想使用wait()或者是notify(),就必须搭配synchronized,需要先获取到锁,然后才能谈wait())
- 需要满足一定的条件才能被唤醒,唤醒后重新尝试获取到这个锁
代码案例
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object){
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
}
- 从结果我们可以看出现在这个线程已经进入了阻塞队列中
- 如果我们不对这个程序做什么的话这个线程就会永远等待下去,此时我们需要做点什么来使得这个线程等待状态结束,这里我们引入了notify方法
线程唤醒
- Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。
- 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。
- 线程通过调用其中一个 wait 方法,在对象的监视器上等待。
- 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。
- 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;
- 例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
- 类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程
notify()方法
- 也需要放到synchronized中使用
- notify操作是一次唤醒一个线程,如果是有多个线程都在等待中,调用notify就相当于是随机唤醒了一个,其它线程保持原状
- 调用notify是通知对方被唤醒,但是调用notify方法的线程并不是立刻释放锁,而是要等待当前的synchronized代码块执行完才释放锁(notify本身不会释放锁)
代码实现
public class WaitTask implements Runnable{
private Object locker=null;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
//进行wait的线程
synchronized (locker){
System.out.println(Thread.currentThread().getName()+"wait开始前");
try {
//直接调用wait,相当于this.wait(),也就是针对WaitTask的对象进行等待
//但是我们需要在下面在NotifyTask类中针对同一个对象进行通知
//然而,在NotifyTask中拿到WaitTask的对象并不容易
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"wait结束");
}
}
}
public class NotifyTask implements Runnable{
private Object locker=null;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
//进行notify的线程
synchronized (locker) {
System.out.println(Thread.currentThread().getName()+"notify开始前");
locker.notify();
System.out.println(Thread.currentThread().getName()+"notify结束");
}
}
}
启动
public class Main {
public static void main(String[] args) throws InterruptedException {
//所以为了解决上述问题,我们需要专门建一个对象,去负责加锁/通知操作
Object locker=new Object();
Thread t1=new Thread(new WaitTask(locker),"线程A:");
Thread t2=new Thread(new NotifyTask(locker),"线程B:");
Thread t3=new Thread(new WaitTask(locker),"线程C:");
Thread t4=new Thread(new WaitTask(locker),"线程D:");
t1.start();
t3.start();
t4.start();
Thread.sleep(3000);
t2.start();
}
}
运行结果
线程D:wait开始前
线程A:wait开始前
线程C:wait开始前
线程B:notify开始前
线程B:notify结束
线程D:wait结束
notifyAll()方法
- 相比较notify方法一次只能唤醒一个线程,notifyAll可以唤醒多个,虽然多个线程都被唤醒了,但这几个线程执行还是有先后顺序的原因是这几个线程还要竞争锁
代码实现
NotifyTask类中修改唤醒
@Override
public void run() {
//进行notify的线程
synchronized (locker) {
System.out.println(Thread.currentThread().getName()+"notify开始前");
//locker.notify();
locker.notifyAll();
System.out.println(Thread.currentThread().getName()+"notify结束");
}
}
运行结果
线程A:wait开始前
线程D:wait开始前
线程C:wait开始前
线程B:notify开始前
线程B:notify结束
线程C:wait结束
线程D:wait结束
线程A:wait结束
面试题:wait和sleep的区别
- sleep操作是指定一个固定时间来阻塞等待,而wait既可以指定时间,也可以无限等待
- wait可以通过notify或interrupt或时间到来唤醒,sleep通过interrupt或时间到唤醒
- wait的主要用途是为了协调线程之间的先后顺序,而sleep单纯是让线程休眠,并没涉及到多个线程的配合
多线程JVM内存分配
多个线程共享堆内存和方法区,但是计数器和栈是每个线程私有的
- 程序计数器
- 一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
- “线程私有”的内存。
- Java虚拟机栈
- 用于存储局部变量表、操作栈、动态链接、方法出口等信息。
- “线程私有”的内存。
- 本地方法栈
- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,
- 而本地方法栈则是为虚拟机使用到的native 方法服务。
- Java堆
- 各个线程共享的内存区域。
- 对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。
- Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
- 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区
- 方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域。
- 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。
- Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
线程安全
案例分析
- 多线程实现网络购票,三个人同时买票,总票数为10,用户提交购票信息后
- 第一步:网站修改网站车票数据
- 第二步:显示出票反馈信息给用户
代码
public class Train implements Runnable{
private int num = 0;
private int count = 10;
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true){
if (count <= 0){
break;
}
num++;
count--;
System.out.println(name + "抢到了第" + num + "张票,剩余" + count + "张票");
try {
Thread.sleep(500);//模拟网络延迟
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
启动
public static void main(String[] args) {
Train train = new Train();
Thread t1 = new Thread(train,"朱家宝");
Thread t2 = new Thread(train,"赵梦涛");
Thread t3 = new Thread(train,"陈佳乐");
t1.start();
t2.start();
t3.start();
}
运行结果
赵梦涛抢到了第2张票,剩余8张票
朱家宝抢到了第3张票,剩余7张票
陈佳乐抢到了第3张票,剩余7张票
赵梦涛抢到了第4张票,剩余6张票
朱家宝抢到了第5张票,剩余4张票
陈佳乐抢到了第6张票,剩余4张票
朱家宝抢到了第7张票,剩余3张票
陈佳乐抢到了第7张票,剩余3张票
赵梦涛抢到了第8张票,剩余2张票
陈佳乐抢到了第10张票,剩余0张票
赵梦涛抢到了第10张票,剩余0张票
变量对线程安全的影响
- 实例变量:在堆中。静态变量:在方法区。局部变量:在栈中。
- 实例变量在堆中,堆只有1个。
- 静态变量在方法区中,方法区只有1个。
- 堆和方法区都是多线程共享的,所以可能存在线程安全问题。
- 局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享。
线程同步
与异步的区别
- 异步:线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)
- 同步:线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
同步代码块
将一块代码锁起来,每次只能一个线程执行。一个线程执行完毕同步块,下一个线程才能执行。
原理:
- 对象锁:一个对象对应一把锁。
- 通过该对象锁,锁住某块代码。
- 哪个线程获取到这个对象锁,哪个线程就可以执行同步块中的代码。其他线程阻塞。
- 该线程执行完同步块代码,主动释放锁。其他线程竞争这个对象锁。
public class Train implements Runnable{
private int num = 0;
private int count = 10;
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true){
synchronized (this){
if (count <= 0){
break;
}
num++;
count--;
System.out.println(name + "抢到了第" + num + "张票,剩余" + count + "张票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
同步方法
- 通过当前对象this的锁,锁住整个方法。
- this表示调用的方法所属的对象。
- 哪个线程获得this锁,哪个线程就能访问该方法。
- 注意:多个线程的this必须指向同一个对象,否则没有同步效果
public class Train implements Runnable{
private int num = 0;
private int count = 10;
private boolean flag = false;
@Override
public void run() {
while (!flag){
isFull();
}
}
private synchronized void isFull(){
String name = Thread.currentThread().getName();
if (count <= 0){
flag = true;
return;
}
num++;
count--;
System.out.println(name + "抢到了第" + num + "张票,剩余" + count + "张票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
- 补充:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。
- 静态同步方法:
修饰符 synchronized static 返回值类型 方法名(形参列表){方法体}
(静态方法中不能使用this)表示找类锁。类锁永远只有1把。
如何解决线程安全问题
- 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
- 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
lock方式
- 应用场景不同,不一定要在同一个方法中进行解锁,如果在当前的方法体内部没有满足解锁需求时,可以将lock引用传递到下一个方法中,当满足解锁需求时进行解锁操作,方法比较灵活
- jdk1.5提供的一种同步方式。
- Lock接口:java.util.concurrent.locks.Lock
- void | lock() 获得锁
- void | unlock() 释放锁
- 实现类:ReentrantLock
- 多个线程竞争同一把锁,实现线程同步。和同步块效果一样。
代码实现
public class Train implements Runnable{
private int num = 0;
private int count = 10;
//创建一把锁
private Lock lk = new ReentrantLock(true);
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true) {
//获取锁
lk.lock();
try {
if (count <= 0) {
break;
}
num++;
count--;
System.out.println(name + "抢到了第" + num + "张票,剩余" + count + "张票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lk.unlock();
}
}
}
}
lock和同步块比较
- lock更直观,直接在代码中可以看见锁。
- lock可以解决同步块中出现异常释放锁的问题。
总结
- 线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
- 线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
- 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
- 对于同步,要时刻清醒在哪个对象上同步,这是关键。
- 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
- 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
- 死锁是线程间相互等待锁造成的,在实际中发生的概率非常的小。但一旦程序发生死锁,程序将死掉。
死锁
- 两个线程各有一把锁,同时继续执行又需要对方的锁,出现相互阻塞的现象。
- 避免死锁的原则:顺序上锁,反向解锁,不要回头
死锁的产生
- 资源竞争:多个线程同时竞争同一资源(如共享变量、文件、数据库连接等),如果每个线程都占用了一部分资源并且正在等待其它线程释放其所需的资源,那么就会出现死锁。
- 嵌套锁:多个线程在不同的顺序上请求锁,例如,线程 A 先获取了锁 1,再请求锁 2,而线程 B 先获取了锁 2,再请求锁 1,这样就会产生死锁。
- 线程间等待:多个线程相互依赖,每个线程都在等待其它线程完成某些操作后才能继续执行,但又互相阻塞,导致无法继续进行下去。
案例代码
线程类1
public class MyThread implements Runnable{
private Object a;
private Object b;
public MyThread(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (a){
System.out.println(name+"获得了a锁");
synchronized (b){
System.out.println(name+"获得了b锁");
System.out.println(name+"----------------------------------");
System.out.println(name+"释放了b锁");
}
System.out.println(name+"释放了a锁");
}
}
}
线程类2
public class MyThread2 implements Runnable{
private Object a;
private Object b;
public MyThread2(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (b){
System.out.println(name+"获得了b锁");
synchronized (a){
System.out.println(name+"获得了a锁");
System.out.println(name+"++++++++++++++++++++++++++++++++");
System.out.println(name+"释放了a锁");
}
System.out.println(name + "释放了b锁");
}
}
}
启动
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
MyThread m1 = new MyThread(a,b);
MyThread2 m2 = new MyThread2(a,b);
Thread t1 = new Thread(m1,"线程1");
Thread t2 = new Thread(m2,"线程2");
t1.start();
t2.start();
}
}
解决方案
- 通过线程通信的方式,让其中一方先放弃手中的锁,让对方先用完。对方用完之后,在通知你来用。
- 对象名.wait() :释放对象锁,然后线程阻塞,直到被唤醒才能继续执行。
- 对象名.notify() :唤醒一个等待该对象锁的阻塞线程
- 对象名.notifyAll() :唤醒所有等待该对象锁的阻塞线程
代码示例
MyThread
public class MyThread implements Runnable{
private Object a;
private Object b;
public MyThread(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (a){
System.out.println(name+"获得了a锁");
synchronized (b){
System.out.println(name+"获得了b锁");
System.out.println(name+"----------------------------------");
System.out.println(name+"释放了b锁");
//唤醒等待b锁的一个线程
b.notify();
//唤醒等待b锁的所有线程
//b.notifyAll();
}
System.out.println(name+"释放了a锁");
}
}
}
MyThread2
public class MyThread2 implements Runnable{
private Object a;
private Object b;
public MyThread2(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (b){
System.out.println(name+"获得了b锁");
try {
//释放当前线程b锁 进入阻塞状态
//需要被唤醒 才能结束阻塞 重新进入CPU队列排队
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(name+"获得了a锁");
System.out.println(name+"++++++++++++++++++++++++++++++++");
System.out.println(name+"释放了a锁");
}
System.out.println(name + "释放了b锁");
}
}
}
守护线程
- java语言中线程分为两大类:一类是:用户线程;一类是:守护线程(后台线程)。其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:
- 一般守护线程是一个死循环,所有的用户线程只要结束,
- 守护线程自动结束。
- 注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
- 比如:每天00:00的时候系统数据自动备份。
- 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
- 一直在那里看着,每到00:00的时候就备份一次。所有的用户线程
- 如果结束了,守护线程自动退出,没有必要进行数据备份了。
public class MyThread extends Thread{
@Override
public void run(){
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程池
- 线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,java开发中经常有池化的思想,如 数据库连接池、Redis连接池等。
- 预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量
线程池的工作机制
- 程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。
- 在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。
线程池优点
- 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
- 提高响应速度,任务到达时,无需创建线程即可运行
- 提供更多更强大的功能,可扩展性高
线程池的构造方法
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//救急线程的空闲时间
TimeUnit unit,//救急线程的空闲时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//创建线程的工厂,主要定义线程名
RejectedExecutionHandler handler) //拒绝策略
{
}
参数名 | 参数意义 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 救急线程的空闲时间 |
unit | 救急线程的空闲时间单位 |
workQueue | 阻塞队列 |
threadFactory | 创建线程的工厂,主要定义线程名 |
handler | 拒绝策略 |
-
corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
-
maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
-
keepAliveTime(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
-
unit :存活的时间单位
-
workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。较常用的是 LinkedBlockingQueue 和 Synchronous。线程池的排队策略与 BlockingQueue 有关
队列 说明 ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列。 LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列。 SynchronousQueue 一个不存储元素的阻塞队列,即直接提交给线程不保持它们。 PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列。 DelayQueue 一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。 LinkedTransferQueue 一个由链表结构组成的无界阻塞队列。与 SynchronousQueue 类似,还含有非阻塞方法。 LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列 -
threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
-
handler(线程饱和策略/拒绝策略):当线程池和队列都满了,再加入线程会执行此策略。
策略 说明 AbortPolicy 拒绝并抛出异常。 CallerRunsPolicy 重试提交当前的任务,即再次调用运行该任务的 execute()方法。 DiscardOldestPolicy 抛弃队列头部(最旧)的一个任务,并执行当前任务。 DiscardPolicy 抛弃当前任务。
案例分析
- 客户到银行时,开启柜台进行办理,柜台相当于线程,客户相当于任务,有两个是常开的柜台,三个是临时柜台。2就是核心线程数,5是最大线程数。即有两个核心线程
- 当柜台开到第二个后,都还在处理业务。客户再来就到排队大厅排队。排队大厅只有三个座位。
- 排队大厅坐满时,再来客户就继续开柜台处理,目前最大有三个临时柜台,也就是三个救急线程
- 此时再来客户,就无法正常为其 提供业务,采用拒绝策略来处理它们
- 当柜台处理完业务,就会从排队大厅取任务,当柜台隔一段空闲时间都取不到任务时,如果当前线程数大于核心线程数时,就会回收线程。即撤销该柜台。
线程池的状态
线程池通过一个int变量的高3位来表示线程池的状态,低29位来存储线程池的数量
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
状态名称 | 高三位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
Running | 111 | Y | Y | 正常接收任务,正常处理任务 |
Shutdown | 000 | N | Y | 不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务 |
stop | 001 | N | N | 不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务 |
Tidying | 010 | N | N | 任务全部执行完毕,当前活动线程是0,即将进入终结 |
Termitted | 011 | N | N | 终结状态 |
线程池的主要流程
- 创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
- 提交任务时,线程池会创建线程去处理任务
- 当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
- 当阻塞队列装满时,继续提交任务,会创建救急线程来处理
- 当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
- 当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程
拒绝策略
- 调用者抛出RejectedExecutionException (默认策略)
- 让调用者运行任务
- 丢弃此次任务
- 丢弃阻塞队列中最早的任务,加入该任务
提交任务
// 执行Runnable
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
各种线程池使用
1.单一线程池
- static ExecutorService | newSingleThreadExecutor() 创建一个使用从无界队列运行的单个工作线程的执行程序
- 核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
- 将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
- 如果某线程抛出异常了,会重新创建一个线程继续执行
- 可能造成oom
- 使用场景:多个任务顺序执行
public class TestPool {
public static void main(String[] args) {
//创建单一线程池:有且仅有一个线程
ExecutorService es = Executors.newSingleThreadExecutor();
//创建任务
Task1 t1 = new Task1();
Task1 t2 = new Task1();
Task2 t3 = new Task2();
Task2 t4 = new Task2();
//执行任务
es.execute(t1);
es.execute(t2);
es.submit(t3);
es.submit(t4);
//关闭线程池
es.shutdown();
}
}
public class Task1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"Runnable"+i);
}
}
}
public class Task2 implements Callable<String>{
@Override
public String call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"Callable"+i);
}
return "end";
}
}
ExecutorService的submit和execute方法区别
submit() 执行完毕后有返回值。execute() 执行完毕后没有返回值。
2. 固定线程池
- static ExecutorService | newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。
- 核心线程数 = 最大线程数 没有救急线程
- 阻塞队列无界 可能导致oom
- 使用场景:同时要执行的任务数量比较多的情况,控制线程的数量。
public class TestFixedPool {
public static void main(String[] args) {
//固定线程池:有且仅有几个线程池
ExecutorService fixed = Executors.newFixedThreadPool(2);
//创建任务
Task1 t1 = new Task1();
//Runnable
Task2 t2 = new Task2();
//Callable
Task1 t3 = new Task1();
//Runnable
Task2 t4 = new Task2();
//Callable
//执行任务
fixed.submit(t1);
fixed.submit(t2);
fixed.submit(t3);
fixed.submit(t4);
//关闭线程池
fixed.shutdown();
}
}
3. 可变线程池
- static ExecutorService | newCachedThreadPool() 创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程
- 初始时,线程数量为0,最大线程数无限制 ,救急线程60秒回收。
- 当有任务时,创建线程,执行任务。执行完毕后,放回线程池。
- 空闲线程空闲时间大于指定时间,销毁该线程。
- 队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
- 可能导致线程数过多,cpu负担太大
- 使用场景:同时执行的任务比较少的时候。
public class TestChangePool {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
Task1 t1 = new Task1();
//Runnable
Task1 t2 = new Task1();
//Runnable
Task2 t3 = new Task2();
//Callable
Task2 t4 = new Task2();
//Callable
es.submit(t1);
es.submit(t2);
es.submit(t3);
es.submit(t4);
es.shutdown();
}
}
4. 定时线程池(任务调度线程池)
- static ScheduledExecutorService | newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行
- 任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用
- 使用场景:延迟执行,或者定期执行。
public class TestScheduledPool {
public static void main(String[] args) {
//定时线程池
ScheduledThreadPoolExecutor ste = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(10);
//执行任务
Task1 t1 = new Task1();
//立马执行
//ste.submit(t1);
//定时执行
//ste.schedule(t1,10, TimeUnit.SECONDS);
ste.scheduleAtFixedRate(t1,10,3,TimeUnit.SECONDS);
//关闭线程池
//不能关闭,因为关闭了线程也就销毁了
//ste.shutdown();
}
}
线程池的关闭
- shutdown():会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭
- shutdownNow():会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表
四种线程池比较
5. SpringBoot中使用自定义线程池
配置
# 线程池配置
thread:
pool:
# 核心线程数
corePoolSize: 8
# 最大线程数
maxPoolSize: 16
# 线程队列长度
queueCapacity: 300
# 超过核心线程数的线程所允许的空闲时间
keepAliveSeconds: 300
配置类
@Data
@Component
@ConfigurationProperties(prefix = "thread.pool")
public class ThreadPoolTaskExecutorConfig {
private Integer corePoolSize;
private Integer maxPoolSize;
private Integer queueCapacity;
private Integer keepAliveSeconds;
// 自定义ThreadPoolTaskExecutor线程池
@Bean(name = "customThreadPoolTaskExecutor")
public ThreadPoolTaskExecutor customThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置线程池参数
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix("myExecutor1--");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
// 修改拒绝策略为使用当前线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化线程池
executor.initialize();
return executor;
}
// 自定义ThreadPoolTaskExecutor线程池
@Primary//ThreadPoolTaskExecutor有多个实例对象时,加注解@Primary,告诉spring优先使用哪个实例
@Bean(name = "customThreadPoolTaskExecutor2")
public ThreadPoolTaskExecutor customThreadPoolTaskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置线程池参数
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix("myExecutor2--");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
// 修改拒绝策略为使用当前线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化线程池
executor.initialize();
return executor;
}
}
测试
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
class BootdemoApplicationTests {
@Resource
private UserMapper userMapper;
@Test
void contextLoads() {
System.out.println(userMapper.selectCount());
}
// 会去匹配 @Bean("customThreadPoolTaskExecutor") 这个线程池
//有@Qualifier该注解的会找指定名称的线程池,如果没有@Qualifier则找有@Primary注解的默认线程池
@Qualifier("customThreadPoolTaskExecutor")
@Autowired
private ThreadPoolTaskExecutor customThreadPool1;
// 会去匹配有@Primary注解的 @Bean("customThreadPoolTaskExecutorPrimary") 这个线程池
@Autowired
private ThreadPoolTaskExecutor customThreadPool2;
@Test
public void testCustomThreadPoolTaskExecutor() {
doFirst();
asyncTask();
asyncTask2();
doFinish();
}
private void doFirst() {
log.info("执行异步任务之前");
}
private void asyncTask() {
for (int j = 0; j < 5; j++) {
final int index = j;
customThreadPool1.execute(new Runnable() {
@Override
public void run() {
log.info("###SpringBoot中使用自定义线程池{} 创建线程 异步执行:{}", customThreadPool1.getThreadNamePrefix() ,index);
}
});
}
}
private void asyncTask2() {
customThreadPool2.getThreadPoolExecutor().toString();
for (int j = 0; j < 5; j++) {
final int index = j;
customThreadPool2.execute(new Runnable() {
@Override
public void run() {
log.info("###SpringBoot中使用自定义线程池{} 创建线程 异步执行:{}", customThreadPool2.getThreadNamePrefix(), index);
}
});
}
}
private void doFinish() {
log.info("执行异步任务结束");
}
}
运行结果
2024年05月13日 17:32:41 INFO 83784 --- [ main] com.kgc.BootdemoApplicationTests : 执行异步任务之前
2024年05月13日 17:32:41 INFO 83784 --- [ main] com.kgc.BootdemoApplicationTests : 执行异步任务结束
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor1--4] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor1-- 创建线程 异步执行:3
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor1--5] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor1-- 创建线程 异步执行:4
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor2--5] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor2-- 创建线程 异步执行:4
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor2--3] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor2-- 创建线程 异步执行:2
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor1--2] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor1-- 创建线程 异步执行:1
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor1--3] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor1-- 创建线程 异步执行:2
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor2--4] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor2-- 创建线程 异步执行:3
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor2--2] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor2-- 创建线程 异步执行:1
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor1--1] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor1-- 创建线程 异步执行:0
2024年05月13日 17:32:41 INFO 83784 --- [ myExecutor2--1] com.kgc.BootdemoApplicationTests : ###SpringBoot中使用自定义线程池myExecutor2-- 创建线程 异步执行:0
2024年05月13日 17:32:41 DEBUG 83784 --- [extShutdownHook] o.s.w.c.s.GenericWebApplicationContext : Closing org.springframework.web.context.support.GenericWebApplicationContext@2427e004, started on Mon May 13 17:32:36 CST 2024
2024年05月13日 17:32:41 INFO 83784 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'customThreadPoolTaskExecutor2'
2024年05月13日 17:32:41 INFO 83784 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'customThreadPoolTaskExecutor'
补充:线程数据传递
- 在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。
- 但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的
- 因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。
通过构造方法传递数据
- 在创建线程时,必须要建立一个Thread类的或其子类的实例。
- 因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。
- 并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)
public class MyThread1 extends Thread {
private String name;
public MyThread1(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("hello " + name);
}
public static void main(String[] args) {
Thread thread = new MyThread1("world");
thread.start();
}
}
- 由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。
- 如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。
- 由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。
- 因此,要想避免这种情况,就得通过类方法或类变量来传递数据
通过变量和方法传递数据
- 向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入
- 另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。
- 然后在建立完对象后,通过对象实例逐个赋值
public class MyThread2 implements Runnable {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("hello " + name);
}
public static void main(String[] args) {
MyThread2 myThread = new MyThread2();
myThread.setName("world");
Thread thread = new Thread(myThread);
thread.start();
}
}
通过回调函数传递数据
- 上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。
- 然而,在有些应用中需要在线程运行的过程中动态地获取数据
- 如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。
- 从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的
public class MyData {
public int value = 0;
}
public class MyWork {
public void process(MyData data, int... numbers) {
for (int n : numbers) {
data.value += n;
}
}
}
public class MyThread3 extends Thread {
private MyWork work;
public MyThread3(MyWork work) {
this.work = work;
}
@Override
public void run() {
java.util.Random random = new java.util.Random();
MyData data = new MyData();
int n1 = random.nextInt(1000);
int n2 = random.nextInt(2000);
int n3 = random.nextInt(3000);
work.process(data, n1, n2, n3); // 使用回调函数
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
+ String.valueOf(n3) + "=" + data.value);
}
public static void main(String[] args) {
Thread thread = new MyThread3(new MyWork());
thread.start();
}
}