【javaEE】多线程(基础)
1.❤️❤️前言~🥳🎉🎉🎉
Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。
如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的内容感兴趣,记得关注我👀👀以便不错过每一篇精彩。
当然,如果在阅读中发现任何问题或疑问,我非常欢迎你在评论区留言指正🗨️🗨️。让我们共同努力,一起进步!
加油,一起CHIN UP!💪💪
🔗个人主页:E绵绵的博客
📚所属专栏:1. JAVA知识点专栏
深入探索JAVA的核心概念与技术细节
2.JAVA题目练习
实战演练,巩固JAVA编程技能
3.c语言知识点专栏
揭示c语言的底层逻辑与高级特性
4.c语言题目练习
挑战自我,提升c语言编程能力
5.Mysql数据库专栏
了解Mysql知识点,提升数据库管理能力
6.html5知识点专栏
学习前端知识,更好的运用它
7. css3知识点专栏
在学习html5的基础上更加熟练运用前端
8.JavaScript专栏
在学习html5和css3的基础上使我们的前端使用更高级、
9.JavaEE专栏
学习更高阶的Java知识,让你做出网站
📘 持续更新中,敬请期待❤️❤️
2.认识线程
线程是什么
一个线程就是一个 " 执行流 ". 每个线程之间都可以按照顺序执行自己的代码 . 多个线程之间 " 同时 " 执行着多份代码。
我们设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
为什么要有线程
首先, "并发编程" 成为 刚需。
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU,而并发编程能更充分利用多核 CPU资源。
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。
其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量。线程是轻量级进程。
- 创建线程比创建进程更快。
- 销毁线程比销毁进程更快。
- 调度线程比调度进程更快。
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"和 "协程",关于线程池和协程我后面会再介绍。
进程和线程的区别
之前讲过一个进程一般使用一个或者多个 PCB 来表示,之所以可以由多个PCB表示,是因为其实一个线程才是由一个PCB来表示,进程中有一个或者多个线程。
对于线程,它们的PCB属性跟上篇文章说的一样,也是主要有这七个属性,那么对于一个进程中的线程,它们的属性是相同的吗?
答案是 前三个都是一样的,后面四个不一样。
从而可以得出同一个进程中的若干线程之间,是共用相同的内存资源和文件资源的,线程1 中 new 个对象,线程2 是可以访问到的; 线程1 打开一个文件,线程2 也是可以直接使用的,这样每个线程就不用单独申请内存,可以共用,效率就更高。但是每个线程都是独立的在 cpu 上调度执行.
so可以得出以下结论:
- 进程是包含线程的,每个进程至少有一个线程存在。
- 进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
Java 的线程和操作系统线程的关系
线程是操作系统中的概念。 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。从而达到跨系统(可以用于多个系统)
java中的主线程
对于java中自带一个主线程,main就是java的主线程,在其里面的代码都是主线程中的任务
jconsole观察线程
我们可以通过jconsole这个工具去观察线程,至于怎么使用,这里有一篇文章写的很好:
观测线程的工具——jconsole_观测电脑线程-CSDN博客
3.创建线程
创建线程有两种方法:
方法1——继承Thread类
通过继承
Thread
类并重写run()
方法来创建线程。class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread is running (Thread Class)");System.out.println("Thread name: " + Thread.currentThread().getName());} }public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start(); // 启动线程} }
方法2——实现Runnable接口
通过实现
Runnable
接口来定义线程任务。class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Thread is running (Runnable)");System.out.println("Thread name: " + Thread.currentThread().getName());} }public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start(); // 启动线程} }
在讲完这两个方法后,我们发现run和start两个重要方法,那么它们是干什么的?
run里面用于描述线程干什么任务,通过 start 去创建线程而后执行该任务。
start是Thread中自带的方法,通俗的来说run方法记录这个"事情",而strat就是要执行run里面的"事情"
如果不去调用start,直接调用run的话,就没有创建出新的线程,就是在主线程中执行run任务。
使用匿名内部类创建线程
其实对于上述代码,我们可以用匿名内部类让其更简便。
public class Main {public static void main(String[] args) {// 使用匿名内部类继承 ThreadThread thread = new Thread() {@Overridepublic void run() {System.out.println("Thread is running (Thread - Anonymous Inner Class)");System.out.println("Thread name: " + Thread.currentThread().getName());}};// 启动线程thread.start();}
}
public class Main {public static void main(String[] args) {// 使用匿名内部类实现 RunnableRunnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("Thread is running (Runnable - Anonymous Inner Class)");System.out.println("Thread name: " + Thread.currentThread().getName());}};// 创建线程并启动Thread thread = new Thread(runnable);thread.start();}
}
使用Lambda 表达式创建线程
Lambda 表达式 我们之前学过,可以将匿名内部类更加简洁出现,但只适用于函数式接口,这里只可以用于简化实现runna接口的方法。
public class Main {public static void main(String[] args) {// 使用 Lambda 表达式实现 RunnableRunnable runnable = () -> {System.out.println("Thread is running (Runnable - Lambda Expression)");System.out.println("Thread name: " + Thread.currentThread().getName());};// 创建线程并启动Thread thread = new Thread(runnable);thread.start();}
}
由于
Thread
类本身不是函数式接口,所以无法直接使用 Lambda 表达式
对于匿名内部类和Lambda 表达式如果有忘的同学可以看看这篇文章
【Java数据结构】反射、枚举以及lambda表达式_java反射获取lamda表达式-CSDN博客
4.Thread类及常见方法
Thread类的概念
我们在前面看到了线程的创建需要Thread类,那么Thread类到底是什么呢?
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
每个执行流,也需要有一个对象来描述,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理,
Thread 的常见构造方法
这些刚才都学过,就不继续说了
Thread 的几个常见属性
常见属性说明:
ID 是线程的唯一标识,不同线程不会重复,这里的id和pcb的id是不同的,是jvm自己搞的一套体系,Java代码也无法获取到pcb的id
名称是各种调试工具用到
状态表示线程当前所处的一个情况,这里有六种状态
1.NEW(新建)
线程对象被创建,但尚未启动。此时线程还未开始执行。2.TERMINATED(终止)
线程已经完成了执行,线程对象还存在。3.RUNNABLE(可运行)
线程已经启动,正在执行。4.TIMED WAITING(计时等待)
线程进入一个有时间限制的等待状态。可以通过Thread.sleep(long millis)、Obiect.wait(long timeout)Thread.join(long millis)进行。
5.WAITING(等待)
线程进入等待状态直到其他线程显式地唤醒它。可以通过Obiect.wait()、Thread.join()方法进入此状态
6.BLOCKED(阻塞)
线程因为等待获取一个监视器锁而进入阻塞状态。通常发生在同步代码块或方法中,线程试图获取一个已经被其他线程持有的锁。
对于后面三个状态讲sleep,join,wait以及锁时,才能理解这三种状态。现在看可能有点看不懂,待我在后面讲解。
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有前台线程结束后,才会结束运行。而后台线程无论结束还是不结束,都不影响jvm结束运行
在 java 代码中, main 线程, 就是前台线程另外程序员创建出来的线程,默认情况下都是 前台线程.可以通过上述 setDaemon 方法来把线程设置为后台线程
在 jconsole 中看到的 jvm 中包含一些其他的内置的线程, 就属于后台线程。我不期望这个线程影响JVM的结束,就设为后台线程,举个例子,比如,有的线程负责进行 gc. (垃圾回收),gc 是要周期性持续性执行的.不可能主动结束,要是把他设为前台,进程就永远也结束不了
是否存活,很直白的意思,就是线程是否还存在
是否被中断(isInterrupted)这个属性之后在中断会讲到,这里先打个哑谜。
start()-启动一个线程
start 才是正在的创建线程(在内核中创建pcb),一个线程需要通过run/lambda把线程要执行的任务定义出来,start 才是正在的创建线程,并开始执行
一个 Thread 对象只能 start 一次
线程中断
在 Java 中,线程的中断(Interrupt)是一种协作机制,用于通知线程应该停止当前的任务并退出。线程中断并不是强制终止线程,而是通过设置线程的中断状态来通知线程,线程可以根据中断状态决定如何响应,是否要中断。
操作系统原生的线程中,其实是有办法让别的线程直接被强制终止的,这种设定其实不太好, 所以 java 没有采纳过来,
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记 ,我们称该变量为中断状态。
Java 提供了以下三个与线程中断相关的方法:
方法名 说明 void interrupt()
设置中断状态为true。如果线程正在阻塞状态(如 sleep,
wait,join
),会抛出InterruptedException,并设置中断状态为false
boolean isInterrupted()
检查目标线程的中断状态,不会清除中断状态。 static boolean interrupted()
检查当前线程的中断状态,并清除中断状态。这个用的少。
下面是有关线程中断的使用代码,可以利用以上三个方法结合一些代码 终止线程或者提前终止线程。
public class Main {public static void main(String[] args) {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {try {System.out.println("Thread is running...");Thread.sleep(1000); // 线程休眠 1 秒} catch (InterruptedException e) {System.out.println("Thread was interrupted while sleeping!");// 恢复中断状态Thread.currentThread().interrupt();}}System.out.println("Thread was interrupted!");});thread.start();// 主线程休眠 3 秒后中断子线程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt(); // 中断子线程} }
输出:
Thread is running...
Thread is running...
Thread was interrupted while sleeping!
Thread was interrupted!
在捕获 InterruptedException
后,调用 Thread.currentThread().interrupt()
恢复中断状态。这样可以确保线程能够正确退出。
在该代码中线程陷入死循环,此时通过设计我们就可以通过interrupt去终止该死循环线程。
这里还是写的最简单的逻辑代码,之后会出现更高级的逻辑代码,更加复杂。
等待一个线程——join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
由于线程是"抢占式"执行且并发执行,所以谁先结束每次都是不确定的,如果希望让代码里面的 t 先结束,main后结束,就可以在main中使用线程等待 t.join()。
上述只是join无参数版本的,也就是死等,只要 t 不结束,就会一直等待下去,还要带参数的版本。
在实际开发中,一般很少使用死等这个策略
传入一个时间:传入的时间是最大等待时间,比如写的等待100s,如果100s之内,t 线程结束了之间返回,如果100s到了,t 线程还没有结束不等了!!!继续往下走。
获取当前线程引用
这个方法我们之前用过,现在来看下吧。
它可以获取当前代码所在线程的对象。
休眠当前线程——sleep()
它也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
join()和sleep()的异常
join()
和sleep()
方法都会抛出InterruptedException
,这是因为它们都是可中断的阻塞方法。当线程在调用这些方法时,如果被其他线程中断(通过调用interrupt()
方法),就会抛出InterruptedException
。为了确保程序的健壮性,我们需要在代码中正确处理这个异常。通常有两种方式来处理
InterruptedException
:
使用
throws
声明抛出异常。使用
try-catch
捕获并处理异常。这样就不会编译错误,能正常运行,那么该用哪种方式处理呢?
我认为用第二种是最好的,因为假如没异常,第一种第二种都能正常运行,而真出现异常了,第一种会运行错误,第二种能真正解决。
并且如果在重写的run()方法中throws interruptedException 就会发生编译错误,由于父类不存在throws interruptedException ,重写的方法就不能throws interruptedException ,只能try catch 解决异常。
所以在面对join()和sleep()时,我们都一致用try catch解决
InterruptedException
异常。后面还会学一个wait()方法,它也是跟join()和sleep()一样,也是用try catch解决InterruptedException
异常。
5.线程安全
线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全案例
下面我们给出线程不安全的案例:
首先,我们写一个多线程代码,一个线程负责把一个数加50000次,另一个线程也负责把这个数加50000次。(从0开始加)
class Counter{private int count = 0;public void increase(){count++;}public int getCount() {return count;} } public class Test4 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());} }
按照我们的逻辑,希望最后得到的结果是100000,为什么会出现这么大的偏差呢?其实,这样的运行结果是由于线程不安全导致的。
事实上:上面的count++操作,在CPU指令角度看,本质上是三个操作:
把内存中的数据加载到CPU的寄存器中。(LOAD)
把寄存器中的寄存器进行加一运行算。(ADD)
把寄存器中的数据写回到内存中。(save)如果是单个线程执行,没有问题。但是如果是多个线程并发执行,就可能会出现错误。由于CPU调度线程的顺序是不确定的,因此这两个线程并发执行的过程中,线程1的操作可能会和线程2的操作相互影响,也就是说,这两个线程的命令的排列方式可能有很多种:
从而因为这样就导致了线程不安全。那么怎么解决该问题呢?看下文可知:
解决线程不安全问题
线程不安全问题的原因
线程不安全的原因有多个:
1.多个线程之间的调度顺序是随机的,操作系统使用抢占式执行的策略来调度线程。【根本原因】
2.多个线程同时修改同一个变量,容易产生线程安全问题。一个线程修改同一个变量 =>没事.
多个线程读取同一个变量 =>没事.每次读到的结果都是固定的,
多个线程修改不同的变量 =>没事.你改你的,我改我的,不影响.
3.进行的修改,不是原子性的。如果修改操作,能够按照原子的方式来完成,就不会出现线程安全问题。原子性是指在一个操作在cpu中不可以中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行(Mysql学过)
4.内存可见性引起的线程安全问题。
5.指令重排序,引起的线程安全问题。
要解决该案例我们首先得看是哪个原因造成的? 我们发现是前三个原因造成的。那么我们试着从这三个原因为出发点去解决它。
第一个原因,我们改变不了,因为内核已经是搞好了的,我们自己改也没用
第二个原因通过调整代码结构,尽量避免出现拿多个线程同时改同一个变量,这是一个切入点,但是在Java中,这种做法不是很普适,只是针对一些特定的场景是可以做到的。
第三个原因,这是解决线程安全问题最普适的方案
该问题不是由内存可见性和指令重排序引起的,所以现在先不讲,后面会讲到。
解决方案——synchronized进行加锁操作
我们可以把修改操作改成“原子性”的操作,那么怎么操作呢?
可以进行加锁操作。相当于是把一组操作,打包成一个整体的操作。此处这里的原子性,是通过锁进行“互斥”,当前线程执行的时候,其他线程无法执行。对于加锁,Java引入了一个synchornized关键字。
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待. 进入 synchronized 修饰的代码块, 相当于 加锁 退出 synchronized 修饰的代码块, 相当于解锁。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕 所的 "有人/无人"). 如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态. 如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
从而可以通过该代码去解决问题
class Counter{private int count = 0;synchronized public void increase(){count++;}public int getCount() {return count;}
}
public class Test4 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());}
}
synchronized
由上可知我们可以通过synchronized解决该线程不安全问题,但是具体使用和特质我们还是不清楚,这里详细说下:
synchronized的使用
1.最常见的是synchronized
修饰代码块,并指定锁对象。
public class Counter {private int count = 0;private final Object lock = new Object(); // 锁对象public void increment() {synchronized (lock) { count++;}}public int getCount() {return count;}
}
2.当 synchronized
修饰实例方法时,锁对象是当前实例(this
)。
public class Counter {private int count = 0;public synchronized void increment() {count++;}//该代码约等于如下代码public void increment() {synchronized(this){count++;}
}public int getCount() {return count;}
}
-
多个线程调用同一个
Counter
实例的increment()
方法时,同一时间只有一个线程能够执行该方法。 -
锁对象是当前实例(
this
)。
3.当 synchronized
修饰静态方法时,锁对象是当前类的 Class
类对象。对于一个类来说,只有一个唯一的calss类对象。
public class Counter {private int count = 0;public static synchronized void increment() {count++;}//该代码约等于如下代码public static void increment() {synchronized(Counter.class){count++;}
}public int getCount() {return count;}
}
-
多个线程调用
Counter.increment()
方法时,同一时间只有一个线程能够执行该方法。 -
锁对象是
Counter.class
。
synchronized
的特性
互斥性
synchronized
确保同一时间只有一个线程能够执行被保护的代码块或方法。其他线程必须等待当前线程释放锁后才能获取锁并执行代码。
这个我们之前就讲述过了,这里不多讲。
锁的可重入性
static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();} }
如果用该代码,按照之前对于锁的设定, 第二次加锁的时候, 该线程就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经堵塞了, 也就无法进行解锁操作. 这时候就会 死锁.
这样的锁称为 不可重入锁.
那么可重入锁就是
一个对象可以多次在同一个线程内连续加锁,而不会导致死锁。
在同一个线程连续加锁时,每次加锁,锁的计数器加 1;每次释放锁时,计数器减 1。只有当计数器为 0 时,锁才会被完全释放。
死锁的情况
虽然在synchronized中连续加锁不会出现死锁,但还有其他很多情况会出现死锁,
比如嵌套锁导致的死锁
public class NestedLockDeadlock {// 定义两个锁对象private static final Object lock1 = new Object();private static final Object lock2 = new Object();// 线程1:先获取lock1,再获取lock2public static void thread1() {synchronized (lock1) { // 获取lock1System.out.println("Thread 1: Acquired lock1");try {Thread.sleep(100); // 模拟一些操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) { // 尝试获取lock2System.out.println("Thread 1: Acquired lock2");}}}// 线程2:先获取lock2,再获取lock1public static void thread2() {synchronized (lock2) { // 获取lock2System.out.println("Thread 2: Acquired lock2");try {Thread.sleep(100); // 模拟一些操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1) { // 尝试获取lock1System.out.println("Thread 2: Acquired lock1");}}}public static void main(String[] args) {// 创建两个线程Thread t1 = new Thread(NestedLockDeadlock::thread1);Thread t2 = new Thread(NestedLockDeadlock::thread2);// 启动线程t1.start();t2.start();}
}
运行这段代码后,程序会陷入死锁,输出类似如下内容:
Thread 1: Acquired lock1
Thread 2: Acquired lock2
死锁原因
-
线程1 先获取了
lock1
,然后试图获取lock2
。 -
线程2 先获取了
lock2
,然后试图获取lock1
。 -
此时,线程1和线程2都在等待对方释放锁,但它们又都持有对方需要的锁,导致死锁。
如何避免死锁
那么如何避免死锁呢?有四个原因导致死锁,我们从这解决问题
避免死锁问题只需要打破上述四点的其中一点即可,对于第一点和第二点对于Java中是打破不了的,他们都是synchronized的基本特性
从第三点来看,不要让锁嵌套获取即可(但是有的时候必须嵌套,那就破除循环等待)
第四点破除循环等待:约定好加锁的顺序,让所有的线程都按照约定要的顺序来获取锁。
内存可见性
我们在最开始讲到线程安全的时候,聊到了关于线程安全问题总共有五种原因,前面我们讲到了三种,还要两种没有涉及到,那么就来聊聊内存可见性引起的线程安全问题。
内存可见性问题指的是在一个线程修改了共享变量的值之后,其他线程是否能够立即看到(即“看到”最新值)这个修改。如果不能,就可能出现内存可见性问题。
public class ThreadDemon26 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){//等待t1线程输入flag的值,只要不为0就能结束t1线程}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{System.out.println("请输入flag的值");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
从之前的内容可知两个线程都写的情况会造成线程安全问题,那么这段代码有一个线程在写,一个线程在读会不会造成线程安全问题?
答案是会的,内存可见性会导致该问题
那么两个线程都进行读会造成线程安全问题?这里的答案是不会。
这段代码想要表现出来的效果是,t1,t2线程同时运行,通过t2线程中输入的flag的值来控制t1线程是否结束。
可是上文我们先后输入了1,0,2......都没能使t1线程结束,这是为什么呢?
我们看while(flag == 0){};这条语句其实有两个指令
①load
cpu从内存中读取flag的值(load)到cpu的寄存器上②访问寄存器
cpu访问寄存器中存储的flag的值,与0进行比较
①中load的操作(读内存),相较于②中访问寄存器的操作,开销要大很多。(访问寄存器的速度是读内存的一万倍)上述while循环中①②这两条指令整体看,执行的速度非常快,等你scanner几秒钟了,我while循环中①②可能都执行几亿次了(cpu的计算能力非常强)
此时JVM就会怀疑,这个①号load 的操作是否还有存在的必要(节省开销),于是经过load试探很多次发现都是一样的,JVM索性就认为load 的值一直都一样(速度太快了,等不到我们scanner输入flag的值),在load一次后,寄存器保存了它的值,然后把load这个操作给优化掉,只留一个访问寄存器的操作指令,访问之前寄存器中保存的值,大大提高循环的执行速度。这就是内存可见化问题会出现的本质原因。
那么怎么解决该问题呢?我们就用volatile关键词修饰变量。
volatile关键词
对于JVM的优化,都适用于单线程,但不适用于多线程,可能会出现bug。
而volatile关键字,是强制性关闭JVM优化,开销是变大了,但是数据更准了。
volatile都是用来修饰变量的
功能①:解决内存可见性问题,每次访问被volatile修饰的变量都要读取内存,而不是优化到寄存器或者缓存器当中
功能②:禁止指令重排序,对于被volatile修饰的变量的操作指令,是不能被重排序的(这个等会会讲)
对于线程指令是否会发生JVM的优化,我们程序员也很难判定是否发生了,所以更需要通过volatile去避免这种可能存在的问题。
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
指令重排序
指令重排序也是一种在编译器发生的优化过程,它改变了程序原有的指令执行顺序,使程序变得更好。
这在单线程是没问题的,但是在多线程可能会导致bug,所以在多线程中我们需要解决该问题,就要用到volatile修饰重排序操作指令涉及的变量,这样就没问题了。
对于指令重排序问题的相关代码后面讲单例模式会有所涉及到。
6.wait和notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作, 主要涉及到三个方法 wait() / wait(long timeout): 让当前线程进入等待状态. notify() / notifyAll(): 唤醒在当前对象上等待的线程.
wait()
wait 做的事情:
1.使当前执行代码的线程进行等待. (把线程放到等待队列中)
2.释放当前的锁(释放后就可以允许其他线程用该锁了)
3.满足一定条件时被唤醒, 重新尝试获取这个锁
wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
1.其他线程调用该对象的 notify 方法.
2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
3.其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
这样在执行object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就 需要使用到了另外一个方法唤醒的方法notify()。
notify()
notify 方法是唤醒等待的线程.
方法notify()也要在synchronized中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果notify和wait要联动,必须要求notify的调用对象,notify的锁对象,wait的调用对象,wait的锁对象都必须相同。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized之后才会释放对象锁。
下面是一个案例
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}
}
public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();
}
notifyall()
notify方法只是随机唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
notifyAll()
比notify()
更安全,因为它不会随机选择一个线程唤醒,而是让所有线程都有机会重新竞争锁,从而避免了某些线程被永久忽略的问题。所以在大多数场景中,推荐使用notifyAll()
注意:虽然是同时唤醒所有线程, 但是这些线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的依次执行.
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notifyall();System.out.println("notify 结束");}}
}
public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));Thread t4 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();t3.start();t4.start();Thread.sleep(1000);t2.start();
}
wait(long time)
wait(long time)
是 Java 中Object
类的一个方法,用于使当前线程在指定的时间内等待某个对象的监视器。如果在指定时间内没有被唤醒,线程将自动从wait
状态中退出。这个方法是Object
类的wait()
方法的超时版本,允许线程在等待时设置一个最大等待时间。时间是以毫秒为单位。
wait 和 sleep 的对比
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
不同点在于:
wait 需要搭配 synchronized 使用, sleep 不需要.
wait 是 Object 的方法 ,sleep 是 Thread 的静态方法
7.单例模式
单例模式是一种经典的设计模式,是校招中最常考的设计模式之一.
那么啥是设计模式呢?
软件开发中有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 就不会吃亏,起码能保下限。这些套路就是设计模式
那么什么是单例模式呢?单例模式是 Java 中最简单的设计模式之一。单例模式 =>只允许存在单个实例.
如何保证这个类只有一个实例呢?靠程序猿口头保证是否可行?比如, 你在类上写一个注释: 该类只能 new 一个实例,不能 new 多次。这完全在依赖人,人 一定是不靠谱的!!!所以需要让编译器来帮我们做一个强制的检查通过一些编码上的技巧,使编译器可以自动发现咱们的代码中是否有多个实例并且在尝试创建多个实例的时候,直接编译出错,根本上保证对象是唯一实例.=>这样的代码,就称为 单例模式
下面是一个简单的单例模式代码:
public class Singleton{private static final Singleton singleton = new Singleton();private Singleton(){}public static Singleton getInstance() {return singleton;}
}
通过精巧的代码设计,就可以达到只允许存在一个实例对象。通过Singletion.getinstance()可以得到唯一的实例对象。
除该类型代码以外,还有另一种类型的代码,下面讲述一下。
单例模式有两种类型:
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
public class Singleton {private static Singleton singleton;private Singleton(){}public static Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;}}
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用(最上面的代码就是)
对于懒汉式的代码因为是真正要用的时候才创建对象,所以相对于饿汉式来说开销更小,效率更高。
但是虽然效率更高,在多线程中饿汉却比懒汉更加安全,懒汉会触发线程安全问题。下面请看分析。
单例模式中多线程引发的安全问题
两个线程同时调用懒汉的单例模式中的Singletion.getinstance()和两个线程同时调用饿汉的单例模式中的Singletion.getinstance(),哪个会有bug?
饿汉都是读取,由于读读不会触发线程安全,所以饿汉不会引发线程安全问题。
懒汉两个线程都涉及到了修改,由于并发执行,导致可能出现两个对象同时存在,不符合单例模式,发生线程安全问题。
最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子
public static synchronized Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;
}
// 或者
public static Singleton getInstance() {synchronized(Singleton.class) { if (singleton == null) {singleton = new Singleton();}}return singleton;
}
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,锁的开销是很大的,可以说有锁的线程虽然安全,但注定不会高性能,极端情况下,可能会出现卡顿现象。
所以接下来要做的就是优化性能,我们发现,当创建好了对象后不获取锁也不会引发线程安全问题,只有第一次没有对象的时候不获取锁才会引发线程安全问题。所以只有第一次创建对象时才需要加锁,我们就将代码设计成如下逻辑:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例。这样就能减少获取锁的次数
所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁,开销极大,我们改善后的代码如下:
public static Singleton getInstance() {if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singletonsynchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支singleton = new Singleton();}}}return singleton; }
上面的代码已经完美地解决了并发安全+性能低效问题:
第2行代码,如果singleton不为空,则直接返回对象,不需要获取锁;而如果多个线程发现singleton为空,则进入内部获取锁;
第3行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton是否为空,因为singleton有可能已经被之前的线程实例化
其它之后获取到锁的线程在执行到第4行校验代码,发现singleton已经不为空了,则不会再new一个对象,直接返回对象即可
之后所有进入该方法的线程都不会去获取锁,在第一次判断singleton对象时已经不为空了
上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排序问题(我们之前讲过)
单例模式中出现的指令重排序问题
创建一个对象,在JVM中会经过三步:
(1)为singleton分配内存空间
(2)初始化singleton对象
(3)将singleton指向分配好的内存空间
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,此时经过线程的调度执行线程B,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报N异常。
这在单线程是不会出现该问题,多线程会出现。
使用volatile关键字可以防止指令重排序,所以通过volatile修饰跟创建对象有关的变量则可以阻止该问题发生。
public class Singleton {private static volatile Singleton singleton;private Singleton(){}public static Singleton getInstance() {if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singletonsynchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支singleton = new Singleton();}}}return singleton;}}
但无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。但由于反射和序列化在该情况中用的极少,所以这里就没必要详细讲述,只需要清楚就行。
8.阻塞队列
阻塞队列概念
阻塞队列是一种特殊的队列,同样遵循“先进先出”的原则,支持入队操作和出队操作和一些基础方法。在此基础上,阻塞队列会在队列已满或队列为空时陷入阻塞,所以它具有如下特性:
1.当队列已满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
2.当队列为空时,继续出队列也会阻塞,直到有其他线程向队列中插入元素。3.并且它是线程安全的,就是多线程中使用它是不会引发线程安全bug的
那么我们用它是干嘛呢?一般用它实现生产者消费者模型,对于该概念我们下面详细说下:
生产者消费者模型
生产者消费者模型有两种角色,生产者和消费者,两者之间通过缓冲容器来达到解耦合和削峰填谷的效果。类似于厂商和客户与中转仓库之间的关系,如下图:
厂家生产的商品堆积在中转仓库,当中转仓库满时,入仓阻塞,当中转仓库为空时,出仓阻塞。通过上述结构,生产者和消费者摆脱了“产销一体”的运作模式,即解耦合。同时,无论是客户需求暴增,还是厂家产量飙升,都会被中央仓库协调,避免突发情况导致结构崩溃,达到削峰填谷的作用。
同理,根据生产者消费者模型,我们将线程带入到消费者和生产者的角色,阻塞队列带入到缓冲空间的角色,一个类似的模型很容易就搭建起来了。
所以说,阻塞队列对生产者消费者模型是相当重要的。
阻塞队列的作用
①解耦合
作为生产者消费者模式的缓冲空间,将线程(其他)之间分隔,通过阻塞队列间接联系起来,起到降低耦合性的作用,这样即使其中一个挂掉,也不会使另一个也跟着挂掉。(就是降低它们之间的联系性)
②削峰填谷
因为阻塞队列本身的大小是有限的,所以能起到一个限制作用,即在消费者面对突发暴增的入队操作,依然不受影响。
如电商平台在每年双十一时都会出现请求峰值的情况,如下:
而假设电商平台对请求的处理流程是这样的:
因为处理请求需要消耗硬件资源,如果没有消息队列,面对双十一这种请求暴增的情况,请求处理服务器很可能就直接挂掉了。
而有了消息队列之后,请求处理服务器不必直接面对大量请求的冲击,仍旧可以按原先的处理速度来处理请求,避免了被冲爆,这就是‘削峰’。
没有被处理的请求也不是不处理了,而是当消息队列有空闲时再继续流程,即高峰请求被填在低谷中,这就是‘填谷’。
经过‘削峰填谷’之后的请求处理曲线就(大致)变成了下图:
经过阻塞队列的请求量就相比之前稳定很多了
阻塞队列的使用
在 Java 标准库中就提供了现成阻塞队列这样的数据结构:BlockingQueue ,这里 BlockingQueue 是一个接口,实现这个接口的类也有很多:
ArrayBlockingQueue
: 基于数组的阻塞队列。
LinkedBlockingQueue
: 基于链表的阻塞队列。
PriorityBlockingQueue
: 支持优先级的阻塞队列。阻塞队列一般用put和take方法。
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列. BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性,所以不用
阻塞队列的实现
在了解了阻塞队列的使用后,我们就准备实现一个阻塞队列来加深对阻塞队列的理解
实现阻塞队列,我们可以从浅到深的来实现,先实现一个普通队列,再在普通队列的基础上,添加上线程安全,再增加阻塞功能,那么就来普通队列的实现吧。这里我们实现一个环形队列(之前讲过怎么实现,这里直接给代码)
class MyBlockingQueue {//对象公用锁private Object lock = new Object();//String类型的数组,存储队列元素private String[] elems = null;//队首位置private int head = 0;//队尾位置private int tail = 0;//存储的元素个数private int size = 0;//构造方法,用于构建定长数组,数组长度由参数指定public MyBlockingQueue(int capacity) {elems = new String[capacity];}//入队方法public void put(String elem) throws InterruptedException {synchronized(lock) {//已满时入队操作阻塞while(size == elems.length) {lock.wait();}//将元素存入队尾elems[tail] = elem;//存入后,队尾位置后移一位tail++;//实现环形队列的关键,超过数组长度后回归数组首位if(tail >= elems.length) {//回归数组首位tail = 0;}//存入后元素总数加一size++;//当出队操作阻塞时,入队后为其解除阻塞//(入队后队列不为空了)lock.notify();}}//出队方法public String tack() throws InterruptedException {//存储取出的元素,默认为nullString elem = null;synchronized (lock) {//队列为空时出队操作阻塞while (size == 0) {lock.wait();}//出队,取出队首值(不用置空,队尾存入时覆盖)elem = elems[head];//出队后,队首位置后移一位head++;//实现环形队列的关键,超过数组长度后回归数组首位if(head == elems.length) {//回归数组首位head = 0;}//存入后元素总数加一size--;//当入队操作阻塞时,出队后为其解除阻塞//(出队后队列不满)lock.notify();}//返回取出的元素return elem;}
}
之后我们要将其变为阻塞队列,就要改进该代码。
首先由于阻塞队列是线程安全的,所以要用volatile修饰变量,sychronized修饰take和put方法。
然后对于满了或者空了会导致阻塞情况,我们就用wait()去阻塞,notify()则放在take和put方法的最后面去唤醒。
那么现在代码还有问题吗?还是存在的,wait()除了用notify()唤醒还可以用什么唤醒?
还可以用interrupt去强制唤醒并抛出异常,如果用的话此时阻塞队列是满的且退出了if循环,并且让size再加一,此时就会引发bug,所以我们的if必须换成while,这样就不能退出循环,并且继续阻塞。
此时阻塞队列就是没有问题的了。
阻塞队列完全版:
class MyBlockingQueue {private String[] elems = null;private volatile int head = 0;private volatile int tail = 0;private volatile int size = 0;public MyBlockingQueue(int capacity) {elems = new String[capacity];}public synchronized void put(String elem) {while(size == elems.length) {try{this.wait();}catch (Exception e){e.printStackTrace();}}elems[tail] = elem;tail++;if(tail >= elems.length) {tail = 0;}size++;this.notify();}public synchronized String take() {String elem = null;while(size == 0) {try{this.wait();}catch (Exception e){e.printStackTrace();}}elem = elems[head];head++;if(head == elems.length) {head = 0;}size--;this.notify();return elem;}
}
class ThreadDemo5 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);Thread customer = new Thread(()->{while (true) {try {String elem = myBlockingQueue.take();System.out.println("消费元素:-> " + elem);Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消费者");Thread producer = new Thread(()->{int n = 0;while (true) {try {myBlockingQueue.put(n + "");System.out.println("生产元素:-> " + n);} catch (Exception e) {throw new RuntimeException(e);}n++;}},"生产者");// 启动生产者与消费者线程customer.start();producer.start();}
}
9.定时器
定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
定时器的使用
标准库中的定时器 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule . schedule 包含两个参数.
第一个参数是继承timetask抽象类的类实例且内部重写了run方法:指定即将要执行的任务代码(timetask实现了runable接口所以有run方法)
第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer(); timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");} }, 3000);
执行schedule方法的时候,系统把要执行的任务放到timer对象中,与此同时timer对象里头自带一个线程叫做“扫描线程”,一旦时间到扫描线程就会执行刚才安排的任务,执行完所有任务后线程也不会销毁,会阻塞等待直到其他的任务被放到timer对象中再继续执行(就这么重复)
定时器的实现
对于定时器来说,重要的还是怎么用它,实现它主要是加深对它的理解。 所以这里的实现过程逻辑细节等我就不讲了,强烈推荐一个文章,写的很好。
JavaEE 初阶(13)——多线程11之“定时器”_jeecg 定时器-CSDN博客
总代码:
import java.util.PriorityQueue;class MyTimerTask implements Comparable<MyTimerTask> {//此处这里的 time,通过毫秒时间戳,表示这个任务具体啥时候执行private long time;private Runnable runnable;public MyTimerTask(Runnable runnable, long delay) {this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(MyTimerTask o) {//比如,当前时间是 10:30,任务时间是 12:00,不应该执行//如果当前时间是 10:30,任务时间是 10:29,应该执行//谁减去谁,可以通过实验判断return (int) (this.time - o.time);}
}public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {//如果还没添加任务,会不断循环执行判断,出现线程饿死。//continue;//因此,使用wait等待,当添加任务后唤醒locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();} else {//如果还没到任务执行时间,依旧不断循环判断,出现线程饿死。//continue;//因此,使用有等待期限的 wait,计算执行的时间与当前时间的差值//当添加新的任务后,wait 被唤醒,再进行新的判断locker.wait(task.getTime() - System.currentTimeMillis());}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);// 唤醒 waitlocker.notify();}}
}
10.线程池
为什么使用线程池
在前面我们都是通过new Thread() 来创建线程的,虽然在java中对线程的创建、中断、销毁、等值等功能提供了支持,但从操作系统角度来看,如果我们频繁的创建和销毁线程,是需要大量的时间和资源的,那么有没有什么开销更小的方法?
第一种是协程,它可以说是轻量级线程,但是java很少用,多用于go和python。
第二种是线程池,java中多用线程池去解决频繁的创建和销毁线程问题。
那么为啥引入线程池就能够提升效率呢?
1.直接创建/销毁线程,是需要在用户态+内核态配合完成的工作,对于线程池,只需要在用户态即可,不需要内核态的配合,这样开销就更小
2.等线程用完之后,线程池不会销毁该线程,而是让其阻塞,等下次用的时候会再次利用它,所以不用频繁的进行创建和销毁。
线程池最核心的设计思路:复用线程,平摊线程的创建与销毁的开销代价
线程池的使用
java 提供了多种方式来创建线程池,主要通过
Executors
工厂类或直接使ThreadPoolExecutor
类来完成
工厂类Executors(工厂模式)
使用Executors工厂类:
newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程数量由nThreads参数确定。
newCachedThreadPool():创建一个线程数量为动态的线程池,线程数量会根据任务数量动态变化,当长时间没有新任务时,空闲线程会被终止。newSingleThreadExecutor():创建一个单线程的线程池,它只会创建一个线程来执行任务。
newScheduledThreadPool(int corePoolSize):创建一个可以安排任务的线程池,可以指定延迟执行任务或定期执行任务。后面两个我们用的都不多,主要是用前面两个
下面是使用代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);// 创建一个可缓存的线程池(线程数量动态调整)ExecutorService cachedThreadPool = Executors.newCachedThreadPool();} }
这代码我们有个疑点,我们并没有new一个对象,那我们是怎么创建出来对象的呢?
这个问题涉及到工厂模式这种设计模式:
工厂模式是一种常用的设计模式,用于封装对象的创建逻辑。它通过使用方法来创建对象(new在方法内部),而不是直接使用
new
关键字实例化对象。这样可以将对象的创建逻辑与使用逻辑解耦,提高代码的可维护性和可扩展性。这里就是用方法创建出对象,所以涉及到了工厂模式
ThreadPoolExecutor类(直接new)
对于刚才讲的 Executors 本质上是 ThreadPoolExecutor 类的封装.
而对于ThreadPoolExecutor类本身我们提供了更多的可选参数, 可以进一步细化线程池行为的设定.
如下图是 ThreadPoolExecutor类的构造方法:
-
核心线程数(
corePoolSize
):线程池中始终保持的线程数量。这是不会被销毁的。 -
最大线程数(
maximumPoolSize
):线程池中允许的最大线程数量。这种一般涉及到刚才的动态线程池,如果任务多了则创建一些线程,少了的话过了一段时间则会销毁,但核心线程数不变。 -
空闲线程存活时间(
keepAliveTime
):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。 -
任务队列(
workQueue
):其为阻塞队列,用于存储等待执行的任务。要记住,当我们创建线程池时,系统也会同时自动创建一个阻塞队列去存储等待执行的任务,这样效率就更高。 -
线程工厂(
threadFactory
):线程工厂是一个用于创建线程的工具类或接口,它允许用户自定义线程的创建逻辑,开发者可以控制线程的名称、优先级、异常处理等属性,从而更好地管理线程资源。 -
拒绝策略(
handler
):当线程池已满且阻塞队列也已满时,新任务的处理策略。
下面重点讲述一下拒绝策略:
AbortPolicy
:直接抛出RejectedExecutionException
异常。
CallerRunsPolicy
:由提交任务的线程直接执行任务。
DiscardPolicy
:直接丢弃任务,不抛出异常。
DiscardOldestPolicy
:丢弃队列中最老的任务,然后尝试提交新任务。
下面是其创建代码
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, // 核心线程数4, // 最大线程数60, // 空闲线程存活时间TimeUnit.SECONDS, // 时间单位new ArrayBlockingQueue<>(10), // 任务队列,容量为 10Executors.defaultThreadFactory(), // 线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略);
总结一下:
工厂模式创建线程:适合简单的线程池创建场景,代码简单,但灵活性有限。
构造方法创建线程:适合需要灵活配置线程池属性的场景,通过自定义线程池,可以更好地管理线程资源,提高代码的可维护性和可扩展性。
submit
通过线程池.submit(继承runable的类的对象) 可以提交一个任务到线程池中执行.
ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");} });
实现一个线程池
这里就直接上代码了,不多说,重点还是使用线程池,不是实现线程池。
/*** 自定义线程池执行器类* 该类通过实现一个具有固定大小的线程池和一个阻塞队列来管理线程,用于异步执行任务*/
class MyThreadPoolExecutor {// 创建阻塞队列,用于存放待执行的任务// 队列大小设为1000,用于控制并发任务的数量,避免过多任务导致资源耗尽BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(1000);/*** 构造函数,初始化线程池* 创建一个线程,该线程循环从阻塞队列中取任务并执行* 这个线程是线程池中的工作线程,负责执行提交的任务*/public MyThreadPoolExecutor(int n) {for (int i = 1; i <= n; i++) {Thread t = new Thread(() -> {// 无限循环,确保线程池可以持续处理任务,直到程序中断或阻塞队列被清空while (true) {try {// 从阻塞队列中取出一个任务,如果队列为空,则线程被阻塞,直到有任务放入队列Runnable task = blockingQueue.take();// 执行取出的任务task.run();} catch (InterruptedException e) {// 如果线程在等待状态时被中断,抛出运行时异常// 这通常会导致程序异常终止throw new RuntimeException(e);}}});// 启动线程池中的工作线程t.start();}}/*** 提交一个任务到线程池* @param task 需要被执行的任务* 任务被放入阻塞队列中,随后由线程池中的工作线程执行*/public void submit(Runnable task){// 将任务放入阻塞队列,如果队列已满,则操作会阻塞,直到有空间可用blockingQueue.offer(task);}
}
class DemoTest1{public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor ex=new MyThreadPoolExecutor(4);for(int i=0;i<100;i++) {int id = i;ex.submit(()->{System.out.println(Thread.currentThread().getName()+" 任务:"+id);});}}
}
下图为执行结果:
相关文章:
【javaEE】多线程(基础)
1.❤️❤️前言~🥳🎉🎉🎉 Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的…...
0.大模型开发知识点需求综述
文章目录 一、机器学习与深度学习基础二、自然语言处理(NLP)基础三、大模型架构四、训练优化技术五、数据处理与预处理六、分布式训练与并行化策略七、微调方法与参数高效微调八、训练框架、工具与自动化流程九、评估与部署十、前沿技术与未来趋势 已更新…...
WHAT - 前端阻塞场景梳理
目录 前言场景梳理1. JavaScript 执行阻塞主线程场景优化思路具体代码示例1. 长时间运行的同步 JavaScript 代码2. 过多的主线程任务(如频繁的 setTimeout/setInterval)3. 未优化的第三方库或框架初始化逻辑总结 2. 样式计算与布局(Layout&am…...
微前端框架 Qiankun 的应用及问题分析
一、Qiankun 的核心应用场景与优势 多技术栈共存与灵活集成 Qiankun 支持主应用与子应用使用不同技术栈(如 Vue、React、Angular 等),通过 HTML Entry 方式接入子应用,无需深度改造子应用即可实现集成,降低了技术迁移成…...
bash: uwsgi: 未找到命令
针对你提出的“bash: uwsgi: command not found”问题,以下是一些可能的解决方案,请按照步骤逐一排查: 1、检查uwsgi命令是否正确: 确保你输入的命令是uwsgi,而不是uWSGI或其他变体。 2、确认uwsgi是否已安装&…...
HAL库,配置adc基本流程
1. 初始化阶段---cubemx (1) GPIO初始化 函数:HAL_GPIO_Init() 作用:配置ADC引脚为模拟输入模式。 代码示例: // 使能GPIOA时钟 __HAL_RCC_GPIOA_CLK_ENABLE();// 配置PA1为模拟输入 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStr…...
【Unity】 HTFramework框架(六十一)Project窗口文件夹锁定器
更新日期:2025年3月7日。 Github源码:[点我获取源码] Gitee源码:[点我获取源码] 索引 Project窗口文件夹锁定器框架文件夹锁定自定义文件夹锁定限制条件 Project窗口文件夹锁定器 在Project窗口中,文件夹锁定器能够为任何文件夹加…...
网络安全技术整体架构 一个中心三重防护
网络安全技术整体架构:一个中心三重防护 在信息技术飞速发展的今天,网络安全的重要性日益凸显。为了保护信息系统不受各种安全威胁的侵害,网络安全技术整体架构应运而生。本文将详细介绍“一个中心三重防护”的概念,并结合代码示…...
《AJAX:前端异步交互的魔法指南》
什么是AJAX AJAX(Asynchronous JavaScript and XML,异步 JavaScript 和 XML) 是一种用于创建异步网页应用的技术,允许网页在不重新加载整个页面的情况下,与服务器交换数据并局部更新页面内容。尽管名称中包含 XML&…...
Elasticsearch 2025/3/7
高性能分布式搜索引擎。 数据库模糊搜索比较慢,但用搜索引擎快多了。 下面是一些搜索引擎排名 Lucene是一个Java语言的搜索引擎类库(一个工具包),apache公司的顶级项目。 优势:易扩展、高性能(基于倒排索引…...
LLM论文笔记 19: On Limitations of the Transformer Architecture
Arxiv日期:2024.2.26机构:Columbia University / Google 关键词 Transformer架构幻觉问题数学谜题 核心结论 1. Transformer 无法可靠地计算函数组合问题 2. Transformer 的计算能力受限于信息瓶颈 3. CoT 可以减少 Transformer 计算错误的概率&#x…...
那年周五放学
2025年3月7日,周五,天气晴,脑子一瞬间闪过02-05年中学期间某个周五下午,17:00即将放学的场景,那种激动,那种说不上的欣喜感,放学后,先走一段316国道,再走一段襄渝铁路&am…...
002-SpringCloud-OpenFeign(远程调用)
SpringCloud-OpenFeign 1.引入依赖2.编写一个远程调用接口3.测试 1.引入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId> </dependency><dependencyManageme…...
SAP 顾问的五年职业规划
SAP 顾问的职业发展受到技术进步、企业需求变化和全球经济环境的影响,因此制定长远规划充满挑战。面对 SAP 产品路线图的不确定性,如向 S/4HANA 和 Business Technology Platform (BTP) 的转变,顾问必须具备灵活性,以保持竞争力和…...
Pandas使用stack和pivot实现数据透视
Pandas的stack和pivot实现数据透视 经过统计得到多维度指标数据非常常见的统计场景,指定多个维度,计算聚合后的指标 案例:统计得到“电影评分数据集”,每个月份的每个分数被评分多少次:(月份,分…...
图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image
图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image 文章目录 图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image主要创新点模型架构图生成器生成器源码 判别器判别器源码 损失函数需要源码讲解的私信我 S…...
c++ 操作符重载详解与示例
c 操作符重载详解与示例 操作符重载详解一、基本规则二、必须作为成员函数重载的运算符1. 赋值运算符 2. 下标运算符 []3. 函数调用运算符 ()4. 成员访问运算符 ->5. 转型运算符 三、通常作为非成员函数重载的运算符1. 算术运算符 2. 输入/输出运算符 << >> 四、…...
在Spring Boot项目中分层架构
常见的分层架构包括以下几层: 1. Domain 层(领域层) 作用:领域层是业务逻辑的核心,包含与业务相关的实体类、枚举、值对象等。它是对业务领域的抽象,通常与数据库表结构直接映射。 主要组件: 实体类(Entity):与数据库表对应的Java类,通常使用JPA或MyBatis等ORM框架…...
upload-labs详解(1-12)文件上传分析
目录 uploa-labs-main upload-labs-main第一关 前端防御 绕过前端防御 禁用js Burpsuite抓包改包 upload-labs-main第二关 上传测试 错误类型 upload-labs-env upload-labs-env第三关 上传测试 查看源码 解决方法 重命名,上传 upload-labs-env第四关…...
无人机应用探索:玻纤增强复合材料的疲劳性能研究
随着无人机技术的快速发展,轻量化已成为其结构设计的核心需求。玻纤增强复合材料凭借高强度、低密度和优异的耐环境性能,成为无人机机身、旋翼支架等关键部件的理想选择。然而,无人机在服役过程中需应对复杂多变的环境:高空飞行时…...
计算机毕业设计Python+DeepSeek-R1大模型空气质量预测分析(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
【渗透测试】基于时间的盲注(Time-Based Blind SQL Injection)
发生ERROR日志告警 查看系统日志如下: java.lang.IllegalArgumentException: Illegal character in query at index 203: https://api.weixin.qq.com/sns/jscode2session?access_token90_Vap5zo5UTJS4jbuvneMkyS1LHwHAgrofaX8bnIfW8EHXA71IRZwsqzJam9bo1m3zRcSrb…...
学习threejs,Animation、Core、CustomBlendingEquation、Renderer常量汇总
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️Animation常量汇总1.1.1 循…...
2、数据库的基础学习(中):分组查询、连接查询 有小例子
二、分组函数 功能:用作统计使用,又称为聚合函数或者统计函数或组函数 1、分类: sum 求和、avg 平均值、max最大值、min 最小值、count 计算个数 2、参数支持哪些类型 Sum\avg 一般处理数值型数据 max、min 可以数值型也可以字符型…...
Ubuntu搭建最简单WEB服务器
安装apache2 sudo apt install apache2 检查状态 $ sudo systemctl status apache2 ● apache2.service - The Apache HTTP ServerLoaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor prese>Active: active (running) since Thu 2025-03-06 09:51:10…...
如何学习编程?
如何学习编程? 笔记来源:How To Study Programming The Lazy Way 声明:该博客内容来自链接,仅作为学习参考 写在前面的话: 大多数人关注的是编程语言本身,而不是解决问题和逻辑思维。不要试图记住语言本身…...
OpenCV计算摄影学(14)实现对比度保留去色(Contrast Preserving Decolorization)的函数decolor()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 将彩色图像转换为灰度图像。它是数字印刷、风格化的黑白照片渲染,以及许多单通道图像处理应用中的基本工具。 cv::decolor 是 OpenCV…...
K8s 1.27.1 实战系列(七)Deployment
一、Deployment介绍 Deployment负责创建和更新应用程序的实例,使Pod拥有多副本,自愈,扩缩容等能力。创建Deployment后,Kubernetes Master 将应用程序实例调度到集群中的各个节点上。如果托管实例的节点关闭或被删除,Deployment控制器会将该实例替换为群集中另一个节点上的…...
Python第十五课:机器学习入门 | 从猜想到预测
🎯 本节目标 理解机器学习两大核心范式(监督/无监督学习)掌握特征工程的核心方法论实现经典算法:线性回归与K-Means聚类开发实战项目:房价预测模型理解模型评估与调优基础 一、机器学习核心概念(学生与老师…...
python 程序一次启动有两个进程的问题(flask)
0. 背景 写了一个使用 flask 作为服务框架的程序,发现每次启动程序的时候,使用 ps 都能观察到两个 python 进程。 此外,这个程序占用了 GPU 资源,我发现有两个 python 进程,分别占用了完全相同的 GPU 显存 1. 原因 …...
使用jcodec库,访问网络视频提取封面图片上传至oss
注释部分为FFmpeg(确实方便但依赖太大,不想用) package com.zuodou.upload;import com.aliyun.oss.OSS; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; import com.zuodou.oss.OssProperties;…...
MyBatis-Plus 与 Spring Boot 的最佳实践
在现代 Java 开发中,MyBatis-Plus 和 Spring Boot 的结合已经成为了一种非常流行的技术栈。MyBatis-Plus 是 MyBatis 的增强工具,提供了许多便捷的功能,而 Spring Boot 则简化了 Spring 应用的开发流程。本文将探讨如何将 MyBatis-Plus 与 Spring Boot 进行整合,并分享一些…...
python-51-使用最广泛的数据验证库Pydantic
文章目录 1 Pydantic2 models2.1 基本模型应用2.1.1 实例化2.1.2 访问属性2.1.3 修改属性2.2 嵌套模型【Optional】3 Fields3.1 Field()函数3.2 带注释的模式Annotated3.3 默认值3.3.1 default参数3.3.2 default_factory3.4 字段别名3.5 数字约束3.6 字符串约束3.7 严格模式4 A…...
Linux - 网络基础(应用层,传输层)
一、应用层 1)发送接收流程 1. 发送文件 write 函数发送数据到 TCP 套接字时,内容不一定会立即通过网络发送出去。这是因为网络通信涉及多个层次的缓冲和处理,TCP 是一个面向连接的协议,它需要进行一定的排队、确认和重传等处理…...
ADB、Appium 和 大模型融合开展移动端自动化测试
将 ADB、Appium 和 大模型(如 GPT、LLM) 结合,可以显著提升移动端自动化测试的智能化水平和效率。以下是具体的实现思路和应用场景: 1. 核心组件的作用 ADB(Android Debug Bridge): 用于与 Android 设备通信,执行设备操作(如安装应用、获取日志、截图等)。Appium: 用…...
【Pandas】pandas Series unstack
Pandas2.2 Series Computations descriptive stats 方法描述Series.argsort([axis, kind, order, stable])用于返回 Series 中元素排序后的索引位置的方法Series.argmin([axis, skipna])用于返回 Series 中最小值索引位置的方法Series.argmax([axis, skipna])用于返回 Series…...
rv1126交叉编译opencv+ffmpeg+x264
文章目录 🌕交叉编译x264🌙创建build_x264.sh(放在下载的x264目录下)🌙编译过程🌙查看编译后的so文件是否是arm版的 🌕下载编译ffmpeg🌙下载ffmpeg🌙创建编译脚本🌙创建ffmpeg编译路…...
【C++】ImGui:VSCode下的无依赖轻量GUI开发
本教程将手把手带您用纯原生方式构建ImGui应用,无需CMake/第三方库。您将全程明了自己每个操作的意义,特别适合首次接触GUI开发的新手。 环境配置 安装VSCode 作用:轻量级代码编辑器,提供智能提示操作: 官网下载安装…...
BUU44 [BJDCTF2020]ZJCTF,不过如此1 [php://filter][正则表达式get输入数据][捕获组反向引用][php中单双引号]
题目: 我仿佛见到了一位故人。。。也难怪,题目就是ZJCTF 按要求提交/?textdata://,I have a dream&filenext.php后: ......不太行,好像得用filephp://filter/convert.base64-encode/resourcenext.php 耶?那 f…...
Jetpack Compose — 入门实践
一、项目中使用 Jetpack Compose 从此节开始,为方便起见,如无特殊说明,Compose 均指代 Jetpack Compose。 开发工具: Android Studio 1.1 创建支持 Compose 新应用 新版 Android Studio 默认创建新项目即为 Compose 项目。 注意:在 Language 下拉菜单中,Kotlin 是唯一可…...
通过着装人体剪影预测关键点,以获取人体的二维尺寸数据。复现过程包括获取或生成3D人体数据集、生成轮廓图像、训练模型等步骤
根据文献《1_Clothes Size Prediction from Dressed-Human Silhouettes》复现方法,主要通过着装人体剪影预测关键点,以获取人体的二维尺寸数据。复现过程包括获取或生成3D人体数据集、生成轮廓图像、训练模型等步骤。 以下是进行复现的大致步骤…...
力扣HOT100之哈希:49. 字母异位词分组
这道题自己先想了一遍,定义了一个比较字符串的函数,用二重循环和一个数组来实现字符串的比较,若两个字符串是异位词,那么就返回true,否则返回false,在主函数中,同样用一个二重循环来遍历向量中的…...
基于单片机的智慧音乐播放系统研究
标题:基于单片机的智慧音乐播放系统研究 内容:1.摘要 随着科技的飞速发展,人们对音乐播放系统的智能化和个性化需求日益增长。本研究的目的是设计并实现一个基于单片机的智慧音乐播放系统。采用单片机作为核心控制单元,结合音频解码模块、存储模块和人机…...
pytest框架 核心知识的系统复习
1. pytest 介绍 是什么:Python 最流行的单元测试框架之一,支持复杂的功能测试和插件扩展。 优点: 语法简洁(用 assert 替代 self.assertEqual)。 自动发现测试用例。 丰富的插件生态(如失败重试、并发执…...
nginx 代理 redis
kubernetes 发布的redis服务端口为 31250 通过命令查询 [miniecs-88500735 /]$ minikube service redis --url http://192.168.49.2:31250[rootecs-88500735 /]# vi /etc/nginx/nginx.conf配置nginx.conf stream {upstream redis {server 192.168.49.2:31250;}server {liste…...
什么是:分布式贝叶斯推断
什么是:分布式贝叶斯推断 分布式贝叶斯推断(Distributed Bayesian Inference)是一种在分布式计算环境下进行贝叶斯统计推断的方法,旨在利用多节点或多设备的并行计算能力,高效处理大规模数据或复杂模型。其核心思想是将数据、模型或计算过程分解到多个节点上,通过协作完…...
C# 命名空间(Namespace)详解
在C#中,命名空间(Namespace)是一种封装和组织代码的方式,它允许将相关的类、接口、结构体和枚举等类型组织在一起,以避免命名冲突,并提供了一种逻辑上的分组方式。命名空间的使用有助于提高代码的可读性、可…...
ASP.NET Core JWT认证与授权
1.JWT结构 JSON Web Token(JWT)是一种用于在网络应用之间安全传输声明的开放标准(RFC 7519)。它通常由三部分组成,以紧凑的字符串形式表示,在身份验证、信息交换等场景中广泛应用。 2.JWT权限认证 2.1添…...
Docker参数,以及仓库搭建
一。Docker的构建参数 注释: 1.对于CMD,如果不想显示,而是使用交互界面:docker run -ti --rm --name test2 busybox:v5 sh 2.对于CMD,一个交互界面只可以使用一个,如果想多次使用CMD,则用ENTR…...
hooks useModule自定义hooks (二次封装AgGridReact ag-table)自定义表头,自定义表头搜索
场景业务: 多次运用AgGridReact的table 列表 思路: 运用自定义hooks进行二次封装: 通用配置例如:传参的参数,传参的url,需要缓存的key这些键值类 定制化配置例如:需要对table 的一些定制化传…...