目录
1)运行jconsole.exe(在安装的jdk的bin目录里面)
5. synchronized 关键字 - 监视器锁 monitor lock
2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
3. DiscardOldestPolicy(丢弃最旧任务)
方法二:使用Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
ReentrantLock和synchronized的区别是什么?
1.认识线程
1.1概念
1)线程是什么
官方
自身理解
用生活例子理解:线程就像 工厂里一条独立的生产线。工厂要完成很多任务,单靠一条生产线(比如只生产杯子)效率低。有多条生产线(线程),就能同时生产杯子、盘子等不同物品,让工厂整体效率变高。放在电脑里,程序通过线程实现 “同时做几件事”,比如一边下载文件(一个线程)、一边播放音乐(另一个线程)。
2)为什么要有线程
官方
自身理解
在生活中,线程就好比餐厅里的多个服务员。一个服务员(单线程)一次只能为一位顾客点单、上菜、结账,效率很低。但如果有多个服务员(多线程),就可以同时为不同顾客服务,有的点单,有的上菜,有的结账,餐厅就能同时处理更多顾客的需求,整体效率大幅提高 。
3)进程和线程的区别
官方
自身理解
- 资源占用:比如电脑上同时运行的音乐播放器程序和浏览器程序,它们是两个不同的进程,有各自独立的资源。而线程共享所属进程的资源,相当于大房子里不同的人,共享房子里的空间和设施 。
- 独立性:进程相互独立,不同进程之间不能直接共享数据,就像不同房子之间是隔开的。线程之间的隔离性没那么强,同一进程内的线程可以直接访问共享数据,比如一个程序里多个线程可以同时访问程序中的全局变量。
- 上下文切换开销:进程切换时,因为各自独立,要保存和恢复大量信息,开销大,就像把一个大房子里的所有东西都收拾好放到一边,再把另一个大房子的东西拿出来布置,很麻烦。线程切换相对简单,因为共享进程的资源,开销小,类似大房子里不同人的位置变换,没那么复杂。
- 控制方式:进程的创建和管理一般通过操作系统的指令或接口来操作;线程在程序里就可以通过代码灵活创建、销毁和管理,比如在 Java 中可以用代码轻松创建新线程。
4)Java 的线程 和 操作系统线程 的关系
官方
自身理解
- 联系:Java 线程需要依赖操作系统线程来实现底层的运行和调度。就像你用手机 APP(Java 线程),APP 的各种操作最终要靠手机系统(操作系统线程)来执行。Java 虚拟机(JVM )在运行时会和操作系统交互,把 Java 线程映射到操作系统线程上 ,让 Java 线程能利用系统资源执行任务。
- 区别:
- 调度方式:操作系统线程由操作系统内核调度,而 Java 线程是由 JVM 调度。可以理解为,操作系统线程是听从操作系统这个 “大管家” 的安排,Java 线程则是在 JVM 这个 “小管家” 的管理下运行 。
- 上下文切换成本:Java 线程切换时,要先在 JVM 中保存和恢复线程状态等信息,开销比操作系统线程切换更高。比如切换两个操作系统线程像在两个相邻房间快速走动;而切换 Java 线程,就像在房间里还要先整理好东西再出去,更费时间。
- 系统资源占用:操作系统线程有自己的线程栈、线程控制块等系统资源;Java 线程除了这些,还需要 Java 堆内存和栈内存,所以 Java 线程消耗的系统资源略多一些 。
1.2第一个多线程程序
代码
package thread;
// 1.创建一个自己的类,继承这个 Thread
class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 2.根据刚才的类,创建出实例,(线程实例,才是真正的线程)
//new 自己上面自定义的类名
//MyThread t = new MyThread();虽然语法正确,但属于 “子类引用指向子类对象”,缺少多态带来的灵活性
Thread t = new MyThread();
// 3.调用 Thread 的star方法,才会真正调用系统api,在系统内部创建出线程
t.start();
}
}
自身理解
观察的两种方式
1.使⽤ jconsole 命令并且观察线程
1)运行jconsole.exe(在安装的jdk的bin目录里面)
2)运行结果图
3)线程的选择及其解释
2.使用编译器idea断点观察
1.3创建线程
方法1 继承 Thread 类
代码
package thread;
class MyThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello word");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//创建 MyThread 类的实例
Thread t = new MyThread2();
//调⽤ start ⽅法启动线程
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
自身理解
方法2 实现 Runnable接口
代码
package thread;
class MyThread3 implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
//Runnable runnable = new MyThread3();
//Thread t = new Thread(runnable);
//下面是上面两段代码的精简写法
Thread t = new Thread(new MyThread());
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
自身理解
方法3 继承Thread,重写run,但是使用匿名内部类
代码
package thread;
public class ThreadDemo4 {
public static void main(String[] args) {
// 使⽤匿名类创建 Thread ⼦类对象
//new Runnable() { ... }:是一个匿名内部类
//类名 实例
Thread t = new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
自身理解
方法4 实现Runnable,重写run,匿名内部类
代码
package thread;
public class ThreadDemo5 {
public static void main(String[] args) {
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
理解
方法5 [常用/推荐]使用lambda表达式
代码
package thread;
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(()-> {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
自身理解
1.4多线程的优势-增加运行速度
public class ThreadDemo8 {
private static final int ARRAY_SIZE = 100000000;
private static final int THREAD_COUNT = 4;
private static int[] array;
public static void main(String[] args) {
// 初始化数组
array = new int[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = i + 1;
}
// 单线程计算
long singleThreadStartTime = System.currentTimeMillis();
long singleThreadSum = singleThreadSum();
long singleThreadEndTime = System.currentTimeMillis();
System.out.println("单线程计算结果: " + singleThreadSum);
System.out.println("单线程计算耗时: " + (singleThreadEndTime - singleThreadStartTime) + " 毫秒");
// 多线程计算
long multiThreadStartTime = System.currentTimeMillis();
long multiThreadSum = multiThreadSum();
long multiThreadEndTime = System.currentTimeMillis();
System.out.println("多线程计算结果: " + multiThreadSum);
System.out.println("多线程计算耗时: " + (multiThreadEndTime - multiThreadStartTime) + " 毫秒");
}
// 单线程求和方法
private static long singleThreadSum() {
long sum = 0;
for (int i = 0; i < ARRAY_SIZE; i++) {
sum += array[i];
}
return sum;
}
// 多线程求和方法
private static long multiThreadSum() {
SumThread[] threads = new SumThread[THREAD_COUNT];
int chunkSize = ARRAY_SIZE / THREAD_COUNT;
// 创建并启动线程
for (int i = 0; i < THREAD_COUNT; i++) {
int start = i * chunkSize;
int end = (i == THREAD_COUNT - 1) ? ARRAY_SIZE : (i + 1) * chunkSize;
threads[i] = new SumThread(start, end);
threads[i].start();
}
// 等待所有线程执行完毕
for (SumThread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 汇总所有线程的计算结果
long sum = 0;
for (SumThread thread : threads) {
sum += thread.getPartialSum();
}
return sum;
}
// 自定义线程类,用于计算数组部分元素的和
static class SumThread extends Thread {
private final int start;
private final int end;
private long partialSum;
public SumThread(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
partialSum = 0;
for (int i = start; i < end; i++) {
partialSum += array[i];
}
}
public long getPartialSum() {
return partialSum;
}
}
}
/*
单线程计算结果: 5000000050000000
单线程计算耗时: 28 毫秒
多线程计算结果: 5000000050000000
多线程计算耗时: 20 毫秒
*/
2. Thread 类及常见方法
2.1Thread 的创建构造方法
语法
//各个部分的举例子
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
具体设置名称例子
package thread;
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"这是我的线程");
//在 start 之前,设置线程为 后台线程(不能在 start 之后设置)
t.setDaemon(true);
t.start();
}
}
2.2Thread的几个常见属性
语法
isAlive()的例子
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
//这个线程的运行时间大约是 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("start 之前:" + t.isAlive());
t.start();
System.out.println("start 之后:" + t.isAlive());
Thread.sleep(2000);
//2s 之后,线程 t 已经结束了
System.out.println("t 之后:" + t.isAlive());
}
}
2.3 启动⼀个线程 - start()
官方
- 启动方式:在 Java 中,若想让写好的线程代码 “动起来”,需用
Thread
类的start()
方法启动线程。 - 唯一性:对同一个
Thread
对象,start()
只能调用一次。多次调用会让程序 “混乱”,导致错误。 - 本质:调用start创建出新的线程本质上是start会调用系统的api,来完成创建线程的操作
自身理解
- 写好的线程代码:好比一份 “备餐计划”(比如 “准备汉堡:煎肉饼、切蔬菜、组装”)。
Thread
对象:相当于一个 “备餐小组”,这个小组 “拿着” 备餐计划,知道要做什么。start()
方法:类似 “店长下令开工”。店长对这个备餐小组只能下一次 “开工令”(调用一次start()
)。一旦下令,餐厅(电脑系统)会真正安排资源(厨房设备、员工时间等),让小组按计划开始备餐(系统通过 API 创建线程,执行代码)。
如果对同一个备餐小组多次下 “开工令”,店长会困惑、员工也会乱套,程序同理 —— 多次调用start()
会让程序报错,无法正常运行。
2.4 终止⼀个线程
官方
让线程run方法(入口方法)执行完毕
自身理解
- 线程的
run
方法:好比外卖员接到的 “送外卖任务清单”,里面写着要给哪些地址送餐、取餐路线等具体操作。 - 线程终止:当外卖员按照任务清单,把所有订单都送到顾客手里(
run
方法里的代码执行完),没有额外的送餐任务了,此时送外卖这个 “工作流程” 就结束了,相当于线程终止。
简单说,线程就像外卖员的工作,做完run
里定义的所有事,自然就 “下班”(终止)了。
2.5 等待⼀个线程 - join()
官方
- 核心作用:
join()
让一个线程等待另一个线程完成工作,强制设定线程结束的先后顺序。 - 线程执行常态:通常多个线程各自运行,谁先结束是随机的,像多个工人同时干活,完工时间不确定。
- 调整执行顺序:
join()
能干预这种随机性,比如线程 B 调用join()
等待线程 A,就确保线程 A 先结束,线程 B 再结束。 - 阻塞特性:等待过程中,使用
join()
的线程会暂停,无法执行其他操作,这就是 “阻塞”。
自身理解
- 线程类比:组装电脑需要多步骤,比如 “装主板”(线程 A)和 “装系统”(线程 B)。正常情况下,若两人同时做,谁先完成不确定。
join()
的应用:如果负责 “装系统” 的人说:“我得等你装完主板,我再装系统”(即线程 B 对线程 A 调用join()
)。此时,一定是 “装主板” 的人先完成(线程 A 先结束),“装系统” 的人才能开始,最后完成装系统(线程 B 结束)。- 阻塞体现:在等 “装主板” 完成的时间里,负责 “装系统” 的人只能闲着,不能干别的,这就是
join()
导致的阻塞。 - 口诀:谁使用join(),剩下的人都要等他结束才行。
简单理解:join()
就像生活中 “排队接力”,必须等前一个人做完,后一个人才能动手,等待时后一个人只能暂停(阻塞),以此确定做事的先后顺序。
2.6 获取当前线程引用
方法
代码
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
注意
2.7 休眠当前线程
方法
代码
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
自身理解
3. 线程的状态
3.1 观察线程的所有状态
官方
自己理解
假设你去银行办业务,整个流程就像线程状态的变化:
- NEW(新建):你拿了排队号,但还没轮到你去窗口办业务。
- RUNNABLE(可运行):轮到你去窗口,正在办业务(执行中);或者你在窗口前准备好材料,随时能开始办(就绪状态)。
- BLOCKED(阻塞):你想去某个窗口办业务,但窗口被别人占用了,你只能排队等这个窗口 “解锁”。
- WAITING(等待):办业务时,工作人员说 “必须等你朋友来了才能办”,你没期限地干等,直到朋友出现。
- TIMED_WAITING(定时等待):你跟工作人员说 “我先等 10 分钟,10 分钟后朋友没来,我就不等了”,这 10 分钟就是定时等待。
- TERMINATED(终止):业务办完,你离开银行,整个办事流程结束。
通过这个例子理解:线程就像人在银行办事,不同状态对应不同的办事阶段,抢资源、等待条件、定时等待等操作,最终做完事就 “终止”。
4. 多线程带来的的风险-线程安全 (重点)
4.1概念
官方
自身理解
-
线程安全场景:
食堂打饭窗口有明确规则:排队打饭,一次只允许一个人到窗口打饭(相当于代码有 “保护机制”)。不管是只有你一个人打饭(单线程),还是很多人排队打饭(多线程),大家都按规则来,不会乱,最后都能顺利打到饭,这就是 “线程安全”。 -
线程不安全场景:
食堂打饭窗口没有规则,很多人同时挤到窗口抢着打饭(多线程执行无保护的代码)。这时候可能有人插队、有人打饭的量被弄错,甚至饭盆被碰翻(出现 bug)。但如果只有你一个人打饭(单线程),没有争抢,一切顺利。这种 “单线程没问题,多线程就乱套” 的情况,就是 “线程不安全”。
4.2不安全的例子
代码
package thread;
//线程不安全的例子
public class ThreadDemo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 预期结果应该是 10w
// 但是实际的结果并不是这样,而且还不到10w
System.out.println("count = " + count);
}
}
为什么会这样
如果是一个线程执行上述的三个指令,当然没问题
如果是两个线程,并发的执行上述操作,此时就会存在变数!!!(线程之间调度的顺序是不确定的!)
一、要点总结
-
代码核心问题:
- 代码中
count++
操作在单线程下正常,但多线程下会出问题。因为count++
底层由三个 CPU 指令组成:- 读取(load):从内存读数据到 CPU 寄存器;
- 计算(add):寄存器里的数据 +1;
- 保存(save):把结果写回内存。
- 多线程时,CPU 调度线程的顺序不可控,可能导致两个线程的指令交叉执行,最终结果比预期值(10 万)小。
- 代码中
-
线程不安全的本质:
多个线程操作共享数据(如代码里的count
)时,因操作步骤拆分且调度无序,导致数据被错误修改。
二、生活例子(以 “两人同时记账” 类比)
假设你和朋友共用一个记账本,记录家庭总支出。每次花钱,就在本子上的数字基础上加 1。
- 理想流程(单线程):
你先拿本子(读取当前数字)→ 计算加 1(如从 5 算成 6)→ 写回本子(保存结果)。朋友等你完成后再操作,最后数字一定正确。 - 混乱场景(多线程):
- 你和朋友同时抢着拿本子(两个线程同时 “读取数据”);
- 你们各自在心里把数字加 1(各自 “计算”);
- 然后你们都把加 1 后的数字写回本子(“保存”)。但本子上最终只加了 1(比如从 5 变成 6),而不是加 2。
原因:朋友看到的是你操作前的数字,你们的操作互相覆盖了。这就像代码里的count++
,多线程交叉操作导致结果错误,体现了线程不安全。
4.3线程不安全的原因
1.根本原因
操作系统中线程采用 “抢占式执行” 和 “随机调度” 机制,线程间执行顺序存在不确定性,为线程安全问题埋下隐患。
2.代码结构因素
仅当多个线程同时修改同一个变量时,才会引发线程安全问题。以下情况无安全风险:单个线程修改一个变量;多个线程仅读取同一个变量(变量内容不变);多个线程修改不同变量。
3.直接原因
多线程对共享变量的修改操作不具备 “原子性”。例如 count++
会被拆解为多个 CPU 指令,若线程执行到一半被调度切走,其他线程介入操作,就会破坏数据一致性。而单个 CPU 指令本身是原子的(要么不执行,要么完整执行)。
4.4 解决之前的线程不安全问题
1.针对 “系统抢占式执行”(根本原因)
无法干预,因操作系统内核已实现该机制,修改等同于开发新系统,难以推广使用。
2.针对 “代码结构”(多线程改同一变量)
分情况处理,部分代码结构可调整,部分受限于业务逻辑等因素无法调整。
3.针对 “操作非原子性”(直接原因)
通过加锁解决。锁具备互斥、排他特性(如 Java 的synchronized
关键字),将多个非原子操作(如count++
拆解的多个 CPU 指令)“打包” 为原子操作,确保执行的整体性,避免线程安全问题。
自身理解
5. synchronized 关键字 - 监视器锁 monitor lock
5.1 synchronized 的特性
1) 互斥
官方
自身理解
排队等待窗口打饭刷钱,如果不刷钱,下一位同学就不打饭刷钱。
2) 可重入
官方
自身理解
整个房子只有一道指纹锁(大门)。你用指纹打开大门后,屋内所有房间都不需要再单独开锁,可以自由进出。
5.2 synchronized 使用示例
1) 修饰代码块: 明确指定锁哪个对象.
锁任意对象
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
5.3 Java 标准库中的线程安全类
不安全
ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder
安全
6. volatile 关键字
官方
其中一个核心功能,就是保证内存可见性
(另一个功能,禁止指令重排序)
功能
- volatile 能保证内存可见性:让多个线程都能及时看到变量的最新值,避免重复做 “无效读取”(类似文段里重复执行结果不变的 load 操作)。
- 禁止指令重排:确保相关操作按代码顺序执行,不乱序。
-
volatile 不保证原子性:
自身理解
想象全班同学靠班级公告栏获取作业通知。
- 数据可见性:老师更新作业通知后,
volatile
就像立刻给全班 “广播”,每个同学都能马上看到最新通知,不会有人还盯着旧内容看。 - 禁止指令重排:老师贴通知时,
volatile
会确保通知内容按正确顺序张贴(比如先写语文作业,再写数学作业),不会颠倒顺序,避免混乱。
7. wait 和 notify notifyAll
官方
功能
7.1 wait()方法
概述
它们和 join
类似,作用是让线程按规定顺序执行。虽然计算机系统对多个线程的调度是随机的,但通过 wait
和 notify
,能在应用层面 “干预” 线程顺序 —— 让后执行的线程主动等待,先让前面的线程完成任务。
自身理解
假设 “煮饭线程” 没完成时,“炒菜线程” 用 wait
等着(主动放弃先做的机会)。
什么情况下使用
就像排队买奶茶。你排到窗口时,发现想喝的奶茶原料没了(条件不具备),这时候你不会一直占着位置,而是先站到旁边等着(放弃竞争)。直到店员把原料准备好了(其他 “线程” 让条件满足),你再去排队窗口参与购买(解除阻塞,重新竞争)。
wait注意点
-
wait 的两种等待方式
- 无超时等待(死等):一直等,没结果就不罢休,像等公交,不设时间限制,一直等到车来。
- 有超时等待:设定等待时间(单位毫秒),时间到了没人 “通知” 就不等了。比如等公交只等 30 分钟,30 分钟车不来就走。
- 注意:死等是下策,程序设计里要像生活中 “容错” 一样,考虑灵活性,不能一直僵死等待。
-
wait 和 notify 的 “配对” 规则
- 必须通过同一个 “对象” 联系。比如借教室:
- 用 A 教室登记等待(object1.wait ()),就得 A 教室的人发通知(object1.notify ())才能唤醒。如果用 B 教室发通知(object2.notify ()),A 教室等待的人不会被唤醒。
- 若多个线程用同一个对象等待,notify 会随机唤醒其中一个,像多个同学用同一教室登记等待,老师喊一声(notify),随机唤醒一个同学。
- 必须通过同一个 “对象” 联系。比如借教室:
代码1
package thread;
//wait的简单使用
public class ThreadDemo24 {
public static void main(String[] args) throws InterruptedException {
//随便拿个对象,都可以进行wait!!
Object object = new Object();
//直接调用wait,会出现非法监控异常
//synchronized (object) {
// object.wait();
//}
synchronized (object) {
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
}
}
注意点
wait的内部做了三件事
1.释放锁
2.进入阻塞等待
3.当其他线程调用notify的时候
报错原因
因为,你都没有锁,那有什么可以释放的呢?等同于,先拿到衣服再筛选适不适合!
解决方案
因此,wait解锁必然是解的object的锁
后续wait被唤醒之后,重新获取锁,当然还是获取到object的锁!!
其他的调用notify的线程,也是需要使用同样的对象
代码2
package thread;
public class ThreadDemo25 {
public static void main(String[] args) {
//需要有一个统一的对象进行加锁,wait,notify
Object locker = new Object();
Thread t1 = new Thread(() ->{
synchronized (locker){
System.out.println("t1 wait 之前");
try {
//wait需要放在synchronized里面
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() ->{
try {
Thread.sleep(5000);
//notify和一样wait都需要放在synchronized里面
synchronized (locker) {
System.out.println("t2 notify 之前");
//呼喊/通知
locker.notify();
System.out.println("t2 notify 之后");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
}
}
运行结果
运行流程
程序中两个线程(t1 和 t2)使用 wait
和 notify
时的执行过程:
- t1 先拿到 “锁”(类似拿到资源使用权),打印信息后进入
wait
状态,释放锁并等待; - t2 等待一段时间后拿到锁,打印信息,用
notify
唤醒 t1; - 但 t2 没释放锁时,t1 想重新拿锁会暂时 “卡住”(阻塞),直到 t2 释放锁,t1 才能继续执行。
自身理解
想象两个人用同一台 ATM 机:
- 甲(t1):先走到 ATM 前(拿到锁),发现机器没钱,就站到旁边等着(释放位置,进入等待)。
- 乙(t2):说 “我先处理点事,5 分钟后再来”(sleep 5000)。5 分钟后,乙走到 ATM 前(拿到锁),操作前喊一声:“钱已经存进去啦!”(notify,唤醒甲)。
- 但乙还没离开 ATM 时(未释放锁),甲想回去用 ATM 会被挡住(阻塞)。直到乙用完离开(释放锁),甲才能重新用 ATM(继续执行)。
7.2 notify()方法
概述
与wait()方法一致
自身理解
notify
通知),“炒菜线程” 再开始炒菜。
这样就能保证 “先煮饭,再炒菜” 的顺序,虽然厨房(类比计算机系统)本来可能随机安排任务,但通过人为约定(
wait
和
notify
),让任务按计划顺序进行。
7.3 notifyAll()方法
官方
唤醒同一个对象上所有等待的线程。
自身理解
多个同学都用 A 教室登记等待,老师喊 “所有人来教室”(notifyAll),大家都被唤醒。但之后大家要重新 “抢” 教室(竞争锁),按顺序使用,谁先抢到不确定。因此,相比全部唤醒(notifyAll),单独唤醒(notify)更好控制。
多线程的小结
8. 多线程案例
8.1 单例模式
官方
单例模式指一个类在程序运行(进程)中,只允许创建一个实例(对象)。
自身理解
想象班级的钥匙管理。教室门锁只需要一把钥匙(对应单例模式中的 “一个实例”)就能开门。如果每个同学都私自配一把钥匙(创建多个实例),不仅容易混乱(不知道谁该用钥匙),还浪费资源(钥匙多了没必要)。
单例模式就像制定规则:班级只允许存在一把官方钥匙,专人管理,避免多余钥匙出现,既规范又节省资源。
饿汉模式 - 单线程版
代码
package thread;
//多线程的例子 -- 单线程 -- “饿汉模式”
//就期望这个类只有一个实例(一个进程中)
class Singleton{
//1.创建好这个实例,通过静态成员保持
private static Singleton instance = new Singleton();
//2.静态方法,获取当前的实例
public static Singleton getInstance(){
return instance;
}
//3.将构造方法进行私有化,外部不能进行 new
private Singleton(){}
}
public class ThreadDemo26 {
public static void main(String[] args) {
//此时 new 就会报错
//Singleton s = new Singleton();
//只能使用规定的方法,而且这个不管写几次,都是获取同一个对象
Singleton s =Singleton.getInstance();
Singleton s2 =Singleton.getInstance();
System.out.println(s == s2);
}
}
代码解释
流程
- 代码实现:类中定义一个静态属性(如
instance
)存储唯一实例,同时将类的构造方法设为私有(private Singleton()
)。这样其他代码无法随意创建新实例,只能通过类提供的getInstance
方法获取已经创建好的实例。 - “饿汉” 含义:类在加载时就立刻创建实例,就像很 “迫切” 地早早完成实例创建,程序一启动实例就存在了。
自身理解
想象一家 24 小时便利店,店里只需要一个 “总收银系统” 来管理所有收款。
- 饿汉模式实现:便利店一开门(程序启动,类加载),就立刻准备好唯一的 “总收银系统”(创建实例)。
- 私有构造方法:把创建收银系统的方式设为 “内部专用”(私有构造方法),员工不能自己随便再弄一个收银系统(其他代码无法
new
新实例),只能使用已经准备好的那个收银系统(通过getInstance
获取实例)。这样就保证整个便利店永远只有一套收银系统,避免混乱和资源浪费。
懒汉模式-单线程版
代码
package thread;
//就期望这个类只能有唯一的实例(一个进程中)
class SingletonLasy{
//这个引用指向唯一实例,这个引用先初始化为 null,而不是立刻创建实例
//(这个是懒汉和饿汉的区别,饿汉直接new)
//private static SingletonLasy instance = null;
private volatile static SingletonLasy instance = null;
private static Object locker = new Object();
public static SingletonLasy getInstance(){
//为什么在最外层又套一层instance == null?
//因为我第一次就创建过了,后续不想创建浪费时间空间 -- (需要的时候就加锁,不需要就不加)
// 如果 instance 为 null,就说明是首次调用,首次调用就需要考虑安全问题,就要加锁
// 如果非 null,就说明是后续的调用,就不必要加锁了
if(instance == null){
//加锁
synchronized (locker) {
if(instance == null) {
instance = new SingletonLasy();
}
}
}
return instance;
}
private SingletonLasy(){}
}
public class ThreadDemo27 {
public static void main(String[] args) {
//调用静态方法getInstance,不需要使用new的
SingletonLasy s1 = SingletonLasy.getInstance();
SingletonLasy s2 = SingletonLasy.getInstance();
System.out.println(s1 == s2);
}
}
代码解释
流程
- 类的定义:定义了一个名为
SingletonLasy
的类,这个类的目标是在整个进程中只有一个实例。 - 静态成员变量:
private volatile static SingletonLasy instance = null;
这行代码声明了一个静态的、私有的、volatile
修饰的instance
变量,初始值为null
。volatile
关键字的作用是保证变量在多线程环境下的可见性,避免指令重排序。 - 静态锁对象:
private static Object locker = new Object();
声明了一个静态的、私有的锁对象locker
,用于在多线程环境下进行同步操作。 - 获取实例的方法:
getInstance()
方法是一个静态方法,用于获取SingletonLasy
类的唯一实例。在方法内部,首先检查instance
是否为null
,如果为null
,则进入同步块。在同步块内部,再次检查instance
是否为null
,如果仍然为null
,则创建一个新的SingletonLasy
实例并赋值给instance
。这样做的目的是为了避免多个线程同时创建实例,保证线程安全。 - 私有构造方法:
private SingletonLasy()
是一个私有构造方法,这意味着外部代码无法直接通过new
关键字来创建SingletonLasy
类的实例,只能通过getInstance()
方法来获取实例。 - 主方法:在
main
方法中,通过调用SingletonLasy.getInstance()
方法两次,分别获取两个实例s1
和s2
,然后比较它们是否相等。由于单例模式保证了类只有一个实例,所以s1
和s2
应该是同一个对象,因此s1 == s2
的结果为true
。
自身理解
想象有一个非常珍贵的古董花瓶,全世界只有一个。这个花瓶存放在一个博物馆里,博物馆有一个专门的窗口来提供给游客参观这个花瓶。
- 单例类:这个古董花瓶就相当于
SingletonLasy
类,它是独一无二的。 - 静态成员变量:博物馆的工作人员知道花瓶的存放位置,这个位置信息就相当于
instance
变量,它指向唯一的花瓶。 - 锁对象:博物馆的窗口有一个门,这个门就相当于
locker
锁对象。当有游客想要参观花瓶时,需要先通过这个门进入参观区域。 - 获取实例的方法:游客想要参观花瓶,只能通过博物馆的窗口来申请参观。工作人员会先检查是否有其他游客正在参观(相当于检查
instance
是否为null
),如果没有,就打开门让游客进入参观区域(相当于加锁),然后再次确认花瓶是否还在那里(再次检查instance
是否为null
),如果在,就允许游客参观(创建实例)。 - 私有构造方法:这个古董花瓶是独一无二的,不能再复制一个新的花瓶,就像
SingletonLasy
类的私有构造方法一样,外部代码无法直接创建新的实例。
注意
8.2 阻塞队列
阻塞队列是什么
一、阻塞队列:线程安全的 “智能排队系统”
文字总结:
- 特性:
- 线程安全:多线程操作时,不会出现混乱(比如多个线程同时添加或获取元素,结果准确无误)。
- 阻塞特性:
- 队列满时:新元素想加入(入队列),得等着,直到队列有空位。
- 队列空时:取元素(出队列)的操作也得等着,直到队列有元素。
- 类比生活:
像医院的挂号窗口。如果挂号单堆满了(队列满),新患者挂号(入队列)得等;如果没挂号单了(队列空),护士处理挂号(出队列)也得等新患者来。
二、生产者消费者模型:分工协作的 “包饺子流水线”
文字总结:
- 角色分工:
- 生产者:生产数据并放入阻塞队列(如擀饺子皮)。
- 消费者:从阻塞队列取出数据处理(如包饺子)。
- 优势:让多线程分工明确,提升效率,避免混乱。
结合包饺子例子:
- 流程:
- 和面(准备工作,单线程完成)。
- 擀饺子皮(生产者,多线程同时擀皮)。
- 包饺子(消费者,多线程同时包)。
- 协作细节:
- 桌子充当阻塞队列:
- 若擀皮太快,桌子堆满饺子皮(队列满),擀皮的人得停下等。
- 若包饺子太快,桌子没饺子皮了(队列空),包饺子的人得停下等。
- 桌子充当阻塞队列:
1.解耦合
概念总结:
解耦合是让两个关联的部分,通过中间层联系,减少直接依赖。就像两个人通过翻译交流,翻译就是中间层。即使一方说话方式变了,另一方也能通过翻译理解,系统更灵活稳定。
生活例子:
- 快递场景(对应第二张图):
你(A)在网上买东西,商家(B)发货。如果没有快递站(中间层),你和商家得直接对接物流,依赖很强。有了快递站后,你填地址给商家,商家把货发往快递站,快递站再配送。你和商家不用管物流细节,依赖降低。就算快递站换了配送路线,对你和商家影响也小。 - 外卖场景:
你(A)点外卖,餐厅(B)做菜。外卖平台是中间层。你通过平台下单,餐厅通过平台接单。即使餐厅换了菜单,或你换了口味,双方通过平台对接,不会直接互相干扰,这就是解耦合。
2.削峰填谷
概念总结:
削峰填谷是一种 “调节波动” 的机制。当资源(如水、数据、任务等)短时间内大量涌入(高峰),或突然减少(低谷)时,通过中间 “存储缓冲环节” 平衡波动。高峰时存储多余资源,低谷时释放资源,让整体使用更平稳,避免高峰冲击或低谷浪费。
生活例子:
- 水库场景(对应第一张图):
三峡大坝就像 “削峰填谷小能手”。上游雨量大,水来得又快又急(高峰),大坝关闸蓄水,把水暂时存起来;上游雨量少,水不够用(低谷),大坝开闸放水。这样下游的水流量始终稳定,既不会被急流冲垮,也不会没水用。 - 用电场景:
白天工厂、家庭用电多(用电高峰),晚上用电少(用电低谷)。电力公司会在低谷时,用多余电力把水抽到高处水库;高峰时,放水库的水发电。这就是用电的 “削峰填谷”,平衡电力供需。
- 削峰填谷:像水库调节水量、电力调节供需,核心是平衡资源波动。
- 解耦合:像快递站、外卖平台,核心是减少模块间直接依赖,让系统更灵活。
三、解耦:分布式系统的 “快递中转站”
文字总结:
- 解耦作用:降低模块间依赖。如分布式系统中,服务器通过阻塞队列协作,不直接交互。
- 好处:
- 某服务器故障,对其他服务器影响小。
- 新增服务器时,原有代码几乎不用改。
类比快递分拣:
- 快递站是 “阻塞队列”,收件点(生产者)把快递放快递站,派件点(消费者)从快递站取件。
- 收件点和派件点不直接联系,通过快递站中转。即使新增收件点或派件点,对现有流程影响很小。
标准库中的阻塞队列
文字总结
- 阻塞队列基础概念
Java 标准库中的BlockingQueue
是一个 “智能排队系统” 接口,有多种实现类:
- ArrayBlockingQueue:像固定容量的杯子,用数组实现,容量固定。
- LinkedBlockingQueue:像可延伸的链条,用链表实现,容量可灵活变化(也可设置固定容量)。
- PriorityBlockingQueue:像 “VIP 排队通道”,元素按优先级排队,优先级高的先处理。
- 核心方法特点
put
方法:往队列加东西时,若队列满了,就 “卡住不动”(阻塞),直到队列有空位。take
方法:从队列取东西时,若队列空了,也会 “卡住不动”(阻塞),直到队列有东西。offer
方法:和put
都是 “排队” 操作,但队列满时,offer
不卡住,直接告诉你 “没加上”(返回结果)。
- 学习知识的层次
- 会用:像会用手机拍照功能,知道按哪个按钮。
- 懂原理:像明白手机拍照如何调用摄像头、处理图像。
- 能实现:像自己设计一个拍照功能,从无到有做出来。对阻塞队列来说,“能实现” 就是自己写出类似功能的代码。
自身理解
-
put
方法:食堂打饭排队
食堂窗口排队打饭,窗口最多容纳 10 人(队列满)。第 11 个人来打饭时,就得在窗口外等着(阻塞),直到有人打完饭离开(队列有空位),才能进去打饭。这就是put
的阻塞效果。 -
take
方法:图书馆借热门书
图书馆某本热门书被借完了(队列空)。这时你去借这本书,就得等着(阻塞),直到有人还书(队列有元素),你才能借到书。这就是take
的阻塞效果。 -
offer
方法:便利店自助结账
便利店的自助结账机最多同时容纳 5 人使用。第 6 个人想用自助结账时,机器直接提示 “已满,请去人工窗口”(返回结果),不会让他等着。这就是offer
不阻塞的特点。
一、技术内容总结
-
加锁与阻塞逻辑
- 加锁位置:在操作队列(入队、出队)时,需通过
synchronized
加锁,确保多线程操作队列的安全性。例如在put
方法中,对locker
加锁,保证同一时间只有一个线程能执行入队操作。 - 阻塞与唤醒:
- 当队列满时,入队线程需阻塞(用
locker.wait()
);当其他线程出队成功(队列不满),需唤醒阻塞的入队线程(用locker.notify()
)。 - 同理,队列空时,出队线程阻塞;入队成功后唤醒出队线程。
- 当队列满时,入队线程需阻塞(用
if
改while
的原因:- 最初用
if
判断队列满 / 空,只检查一次条件。但线程被唤醒时,队列状态可能已变化(比如多个线程等待)。 - 改用
while
后,每次唤醒都重新检查条件。若队列仍满 / 空,继续阻塞,避免 “误唤醒” 导致的逻辑错误。
- 最初用
- 加锁位置:在操作队列(入队、出队)时,需通过
-
多线程编程的难点
- 多线程执行顺序随机,需确保各种执行顺序下程序结果都正确。例如,两个线程同时操作队列,需通过锁、阻塞、唤醒机制,保证数据一致性。
二、生活例子类比
-
排队买奶茶(阻塞与唤醒)
- 场景:奶茶店排队,柜台最多容纳 10 人(队列容量)。
- 阻塞:第 11 个人来买奶茶(队列满),需在门口等待(
wait()
阻塞)。 - 唤醒:当有人买完奶茶离开(出队,队列不满),店员喊 “下一位”(
notify()
唤醒),等待的人才能进店买奶茶。
-
闹钟与起床(
if
改while
的意义)if
的问题:你定了 7:30 的闹钟(if
只判断一次),但可能 6:30 就醒了,若不看时间直接起床,可能太早。while
的优势:每次醒来(被唤醒)都先看时间(重新判断队列条件)。若没到 7:30(队列仍满 / 空),继续睡(继续阻塞);若到时间(队列条件满足),再起床(执行操作)。
通过这些例子可以理解:
- 多线程操作队列时,锁是 “排队规则”,保证秩序;阻塞与唤醒是 “等待和叫号”,协调线程;
while
是更严谨的 “条件检查”,避免意外情况。即使没学过编程,也能通过生活场景掌握这些核心逻辑。
阻塞队列实现
package thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
//阻塞队列
public class ThreadDemo28 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
//进入,为什么不使用offer,因为put可以堵塞
queue.put("aaa");
//有元素的时候就进行弹出元素
String elem = queue.take();
System.out.println("elem = " + elem);
//没有元素的时候就进行堵塞
elem = queue.take();
System.out.println("elem = " + elem);
}
}
//-------
package thread;
//阻塞队列
//为了简单,不写泛型的形式,考虑是单纯的String
class MyBlockingQueue{
private int head = 0;
private int tail = 0;
private String elems[] = null;
private int size = 0;
//准备锁对象,如果使用 this 也可以
//这里创建锁对象是为了更好的展示
private Object locker = new Object();
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
public void put(String elem) throws InterruptedException {
// 锁加到这里和加到方法上本质是一样的,加到方法上面是给 this 加锁
// 此处是给 locker 加锁
synchronized (locker) {
//if只判定一次条件,一旦程序进入阻塞之后,再被唤醒.这中间隔的时间,这个过程中会有很多的变数!
//就难以保证,你的条件是否仍然满足入队列的条件是否具备
//所以将 if 修改为 which
while (size >= elems.length) {
//队列满,后续也需要这个代码阻塞
//那么就可以考虑使用wait,因为不知道要休眠多少时间
//所以不考虑使用sleep
//1.
locker.wait();
}
//新的元素放在 tail 指向的位置上
elems[tail] = elem;
tail++;
if (tail >= elems.length) {
tail = 0;
}
size++;
//2.
locker.notify();
}
}
public String take() throws InterruptedException {
String elem = null;
//引入锁确保线程安全
synchronized (locker) {
while (size == 0) {
//队列空了,后续也需要这个代码阻塞
//2.
locker.wait();
}
elem = elems[head];
head++;
if (head >= elems.length) {
head = 0;
}
size--;
// 元素出队列成功后,加上锁唤醒
//有睡眠就要有唤醒
//1.
locker.notify();
}
return elem;
}
}
public class ThreadDemo29 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue(1000);
// queue.put("aaa");
// queue.put("bbb");
// queue.put("ccc");
// queue.put("ddd");
//
// String elem = "";
// elem = queue.take();
// System.out.println("elem: " + elem);
// elem = queue.take();
// System.out.println("elem: " + elem);
// elem = queue.take();
// System.out.println("elem: " + elem);
// elem = queue.take();
// System.out.println("elem: " + elem);
//此时就来模拟使用队列阻塞来实现,生产者,消费者
//生产者
Thread t1 = new Thread(() ->{
int n = 1;
while (true){
try {
queue.put(n + "");
System.out.println("生产元素 " + n);
n++;
//这里加上睡眠时间,造成消费者消费 > 生产者生产
//从而形成消费者等待生产者
//同理放在消费者睡眠,生产者就会 > 消费者
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2 = new Thread(() ->{
while (true){
try {
String n = queue.take();
System.out.println("消费元素 " + n);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
8.3 定时器
定时器是什么
标准库中的定时器
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
实现定时器
一、技术内容总结
1. Timer(定时器)的核心组成
- 计时线程:相当于一个 “时间管理员”,专门盯着时间,到点就触发任务执行。
- 任务队列:用来存放所有 “预约” 的任务。这些任务带着 “延迟时间”,比如 “3 秒后执行”“5 分钟后执行”。
- 优先级队列:任务按执行时间排序,时间短的任务优先处理。就像排队时,赶时间的人优先办事,避免无意义的扫描全部任务,提升效率。
2. Java 标准库中的 Timer 使用
- 创建 Timer:
Timer timer = new Timer();
相当于买了一个 “定时闹钟”。 - 添加任务:用
schedule
方法安排任务,比如:
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
}, 3000); // 3000 毫秒后执行任务
- 这就像给闹钟设置 “提醒事项”,时间一到,任务(如打印文字)就会执行。
- 结束 Timer:用
timer.cancel();
主动关闭定时器,否则它会一直运行(因为内置的是 “前台线程”,会阻止程序结束)。
二、自身理解
1. Timer 组成类比
- 计时线程:类似餐厅的 “叫号员”,时刻盯着时间,到点就喊顾客用餐。
- 任务队列:像餐厅的 “排队系统”,记录每个顾客的预约信息(比如 “10 分钟后用餐”)。
- 优先级队列:好比 “加急通道”,赶时间的顾客(任务)优先安排,叫号员不用挨个查所有人,只看最急的顾客即可,节省时间。
2. 标准库 Timer 使用类比
- 创建 Timer:相当于买一个 “智能提醒器”。
- 添加任务:比如用提醒器设置 “3 分钟后提醒烧水”,到时间提醒器就会响(执行任务)。
- 结束 Timer:用完提醒器后关掉它,不然它会一直耗电(持续运行,阻止程序结束)。
通过这些类比可以理解:Timer 就是一个 “任务闹钟”,按约定时间执行任务,而背后的线程、队列等机制,就像现实中协调任务的 “管理员” 和 “排队系统”,让一切有序进行。
代码
package thread;
import java.util.PriorityQueue;
//通过这个类,来描述一个任务
//Comparable 接口存在的核心意义,就是强制要求实现类提供 compareTo 方法
class MyTimerTask implements Comparable<MyTimerTask>{
//在什么时间来执行这个任务
//此处约定这个 time 是一个 ms 级别的时间戳
private long time;
//实际任务要执行的代码
private Runnable runnable;
public long getTime(){
return time;
}
// delay 期望是一个 “相对时间”
public MyTimerTask(Runnable runnable,long delay){
this.runnable = runnable;
//计算一下真正要执行任务的绝对时间,(使用绝对时间,方便判定任务是否到达时间的)
// 获取当前系统时间戳 延迟的毫秒数
this.time = System.currentTimeMillis() + delay;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
//是写法1 return (int) (this.time - o.time);
//还是写法2 return (int) (o.time - this.time);
//答案是写出来试试就知道了
return (int) (this.time - o.time);
}
}
// 通过这个类,来表示一个定时器
class MyTimer{
//负责扫描任务队列,执行任务的线程
private Thread t = null;
//任务队列
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
//搞个锁对象,此处使用 this 也可以
// 示例:假设后续有需要加锁的逻辑
// public void someSynchronizedMethod() {
// synchronized (this) { // 直接用 this 作为锁
// // 同步代码块
// }
// }
private Object locker = new Object();
public void schedule(Runnable runnable,long delay){
//加锁
synchronized (locker){
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
//添加新的元素之后,就可以唤醒扫描的线程的 wait 了(不等于null的时候唤醒)
locker.notify();
}
}
public void cancel(){
// 主动结束 t 线程,因为扫描线程创建的是前台进程,不会主动结束
//interrupt
//if (t != null) {
// t.interrupt();
//}
//然后呢在下面的while里面写成!Thread.currentThread().isInterrupted()
}
//构造方法,创建扫描线程,让扫描线程完成判定和执行
public MyTimer(){
//第一个分支(构造函数)
//创建扫描线程 t -------------------
t = new Thread(() ->{
//扫描线程就需要循环反复的扫描队首元素,然后判定队首元素是不是时间到了
//如果时间没到,啥都不干
//如果时间到了,就执行这个任务并且把这个任务从队列中删掉
//扫描的逻辑 -------------------
while (true){
try {
//如果锁加在while外面,就会导致while释放不辽,在 schedule卡住
synchronized (locker) {
while (queue.isEmpty()) {
//暂时先不做处理
//continue;
//需要的时候才判断,不要的时候就不要判断
//null的时候去循环没有意义,停下来还能节省空间
//加上waity以及通知
locker.wait();
}
MyTimerTask task = queue.peek();
//获取当前的时间
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
//当前时间已经达到了任务时间,就可以执行任务了
queue.poll();
task.run();
} else {
//当前时间没有达到了任务时间,暂时先不执行
//不能使用sleep,会错过新的任务,也无法释放锁
//Thread.sleep(task.getTime() - curTime);
locker.wait(task.getTime() - curTime);
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
//要记得star!!! 启动扫描线程 t----------
t.start();
}
}
public class ThreadDemo31 {
public static void main(String[] args) {
//总起点
MyTimer timer = new MyTimer();
//第二个分支(添加任务)
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
System.out.println("hello main");
}
}
流程图
代码解释
规定了main,class MyTimer(定时器),class MyTimerTask(定时器任务)三个部分
main:创建MyTimer,并添加任务
class MyTimer:构建基本条件,创建线程 t 及其功能,启动线程 t
class MyTimerTask:执行任务的逻辑
main部分
class MyTimer部分
class MyTimerTask部分
代码途中的问题(重点)
1.怎么计算时间定时
时间戳+ delay
(相对延迟时间)= 最终得到任务执行的绝对时间
2.任务时间和当前时间的差
3.为什么要加锁
4.加锁的位置
5.加完锁后,所以地方都锁吗
6.等待使用wait还是sleep
在sleep过程中,又有别的线程调用了schedule,安排新的任务,新的任务时间是13:30,就不能进行操作;另外,sleep也不能解锁。
1)wait的过程中,有新的任务来了,wait就会被唤醒!schedule有notify的
根据新的任务重新计算要等待的时间了,你也不知道新的任务是不是最早的任务!!
2)wait过程中,没有新的任务,时间到了.按照原定计划,执行之前的这个最早的任务即可
7.代码执行流程是怎么样
8.4 线程池
线程池是什么
标准库中的线程池

1. corePoolSize
(核心线程数)
含义:线程池中始终保留的 “常驻线程” 数量,即使这些线程暂时没任务,也不会被销毁。
生活实例:像餐厅里 “固定雇佣的全职服务员”。无论餐厅是忙是闲,这些服务员始终在岗,随时准备接待顾客。
2. maximumPoolSize
(最大线程数)
含义:线程池能容纳的线程最大数量,即核心线程 + 临时扩展线程的总和。
生活实例:餐厅里 “全职服务员 + 兼职服务员” 的总人数上限。比如全职有 5 人,最多还能招 3 个兼职,那最大线程数就是 8。
3. keepAliveTime
(存活时间)
含义:当线程数超过核心线程数时,多余的临时线程在空闲状态下能保留的最长时间,超时后会被销毁。
生活实例:餐厅里 “兼职服务员的空闲等待时间”。比如兼职服务员空闲超过 1 小时没活干,就可以下班(销毁)。
4. unit
(时间单位)
含义:给 keepAliveTime
定义时间单位,比如秒、分钟、小时等。
生活实例:给 “兼职空闲等待时间” 定规则,是 “1 小时” 还是 “30 分钟”,这里的 “小时”“分钟” 就是时间单位。
5. workQueue
(任务队列)
含义:存储等待处理任务的队列。当核心线程都在忙时,新任务会先进入队列排队。
生活实例:餐厅的 “顾客候餐区”。当所有服务员都在服务顾客时,新到的顾客需要先在候餐区排队,等有空余服务员再接待。
6. threadFactory
(线程工厂)
含义:创建线程的 “模板”,定义线程如何创建、命名、设置属性等。
生活实例:餐厅的 “招聘渠道”。比如通过中介招服务员(统一培训、统一工服),这里的 “中介” 就像线程工厂,确保每个新线程(服务员)有规范的创建方式。
7. handler
(拒绝策略)
含义:当任务队列满了,且线程数已达到最大线程数时,对新任务的处理策略。
生活实例:餐厅的 “满客处理方式”。比如当候餐区满员、服务员也全在忙时,新顾客来了是直接拒绝(抛异常),还是劝其下次再来(丢弃任务),这就是拒绝策略。
拒绝的4个方法/策略
1. AbortPolicy
(抛异常拒绝)
含义:直接拒绝新任务,还会抛出异常(RejectedExecutionException
),就像明确说 “不伺候了”。
生活实例:
餐厅已满员(队列满、服务员全忙),这时新顾客非要进店,店员直接拒绝:“没位置了!”,顾客一听急了(抛异常),大吵大闹,最后新顾客没进店,店里也被闹得鸡飞狗跳(任务没处理,还引发问题)。
2. CallerRunsPolicy
(提交者自己处理)
含义:新任务不交给线程池,让 “提交任务的线程” 自己执行。相当于 “你催我,那就自己干”。
生活实例:
快递站爆仓(快递太多,员工忙不过来),你去寄快递(提交任务),快递员说:“太忙了,你自己把快递送去目的地吧!” 于是你只能自己跑腿送快递(提交任务的线程自己处理任务)。
3. DiscardOldestPolicy
(丢弃最旧任务)
含义:扔掉队列里最久的老任务,腾出位置给新任务。类似 “喜新厌旧”。
生活实例:
游乐园排队玩项目(任务队列),队伍已满。这时来了个 VIP 游客(新任务),工作人员直接把排队最久的游客(最旧任务)拉出来,让 VIP 游客顶替位置,说:“你下次再来吧,这位先玩!”
4. DiscardPolicy
(默默丢弃新任务)
含义:安静地扔掉新任务,不处理也不提醒,像 “当没看见”。
生活实例:
超市搞促销,免费发鸡蛋,排队的人太多(队列满)。后来的人还想领鸡蛋(新任务),工作人员直接无视,既不解释也不处理,后来的人只能白跑一趟(新任务被丢弃)。
实现线程池
方法
代码
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//简单创建线程池的写法
public class ThreadDemo32 {
public static void main(String[] args) {
//创建线程池 -- 得到一个数量为 4 个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(4);
//往线程池插入数据
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
//------------------
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPoolExecutor{
//将创建的线程都放入 threadList 链表进行保存
private List<Thread> threadList = new ArrayList<>();
//用来保存任务的队列
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
//通过 n 指定创建多少个线程
public MyThreadPoolExecutor(int n){
for (int i = 0; i < n; i++) {
Thread t = new Thread(() ->{
//线程池中的核心代码部分
// 线程要做的事情就是把任务队列中的任务不停的取出来,并且进行执行
while (true){
//取元素
try {
//任何实现了 Runnable 接口的类 或者 使用 Lambda 表达式表示的 Runnable 对象
//都代表着一个可以被线程执行的任务
//在你的线程池代码里,BlockingQueue<Runnable> queue 是用来存储待执行任务的队列。
//当线程池中的线程从队列中取出任务时,这些任务是以 Runnable 对象的形式存在的,所以使用 Runnable 类型的变量来接收。
//(使用 Runnable 来 封装 任务并让线程执行)
//此处的 take 带有阻塞功能的
//如果队列为 空 ,此处 take 就会进行阻塞
Runnable runnable = queue.take();
//取出一个任务即可
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
threadList.add(t);
}
}
//Runnable 接口的核心用途就是定义一个可执行的任务
//Runnable是拿来重写run,来进行我需要进行的操作的
//就相当于,我需要定制操作的东西,就要使用run
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public class ThreadDemo33 {
public static void main(String[] args) throws InterruptedException {
//规定了4个线程 + 1000个任务
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
for (int i = 0; i < 1000; i++) {
//变量捕获,设置为final
//匿名内部类访问了外部循环中非final 且非effectively final"的变量
//这违反了Java语法规则
int n = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + n +",当前线程为:" + Thread.currentThread().getName());
}
});
}
}
}
多线程总结
扩展问题
一、基础概念类
-
线程和进程的区别是什么?
-
答案:线程是进程中的一个执行单元,共享所属进程的资源,而进程是系统分配资源的最小单位,拥有独立的内存空间等资源。线程的创建和销毁开销较小,而进程的开销较大。线程之间相互依赖,一个线程崩溃可能影响整个进程,而进程之间相互独立。
-
生活例子:线程就像一个餐厅里的服务员,他们共享餐厅的厨房、餐具等资源(进程)。多个服务员(线程)可以同时为不同的顾客服务,但如果一个服务员犯了错误(线程崩溃),可能会影响整个餐厅的运营(进程崩溃)。而不同的餐厅(进程)之间是相互独立的,一个餐厅出问题不会影响另一个餐厅。
-
-
线程的生命周期有哪些状态?
-
答案:线程的生命周期包括新建、就绪、运行、阻塞、等待、超时等待和终止等状态。这些状态反映了线程从创建到执行完毕的整个过程。
-
-
生活例子:线程的生命周期就像一个人的一天。新建状态就像刚起床但还没开始活动,就绪状态就像准备好开始一天的工作,运行状态就像在工作,阻塞状态就像在等待某个任务完成(比如等电梯),等待和超时等待状态就像在休息或等待特定时间,终止状态就像一天的工作结束,进入休息时间。
-
-
如何创建线程?有哪些方式?
-
答案:创建线程的方式包括继承
Thread
类、实现Runnable
接口、使用匿名内部类和Lambda
表达式等。 -
生活例子:创建线程就像安排任务给不同的人。继承
Thread
类就像让一个人专门负责某种任务,实现Runnable
接口就像让不同的人按照统一的任务规范去执行,匿名内部类和Lambda
表达式就像临时安排任务给某个人,不需要提前定义好任务执行者。
-
-
线程的常用方法有哪些?
-
答案:线程的常用方法包括
start()
、run()
、sleep(long millis)
、yield()
、join()
、isAlive()
、getName()
和setName(String name)
等。 -
生活例子:
start()
就像告诉某个人开始工作,run()
就是工作的具体内容,sleep(long millis)
就像让某个人休息一段时间,yield()
就像让某个人暂停一下让别人先做,join()
就像等待某个人完成工作后再继续,isAlive()
就像检查某个人是否还在工作,getName()
和setName(String name)
就像获取和设置某个人的名字。
-
二、Thread类
1.start和run有什么区别?
start()
:是线程启动的 “入口操作”。调用start()
时,JVM 会为线程分配资源,使线程进入就绪队列,等待 CPU 调度。调度成功后,线程会自动执行关联的run()
方法,真正实现多线程的并发执行。run()
:是线程业务逻辑的 “载体”,用于封装线程要执行的具体任务(如数据计算、文件读取等)。若直接调用run()
,它只是作为普通方法在当前线程中顺序执行;只有通过start()
启动线程,run()
中的代码才会在新线程中独立运行,这也是多线程并发的核心机制。
简单总结:start()
负责 “启动线程,让线程参与 JVM 调度”;run()
负责 “定义线程要做的事”,二者配合实现多线程功能。
2.如何启动更多线程?
//要想启动更多线程,就是得创建新的对象!!
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
System.out.println("hello1");
});
Thread t2 = new Thread(() ->{
System.out.println("hello2");
});
t1.start();
t2.start();
}
}
3.怎么终止线程?
核心也就是让run方法能够提前就结束=>非常取决于具体代码实现方式了.
方法一:要给标志位上加 volatile 关键字
package thread;
//终止线程--添加全局变量isQuit来控制
//1.能否将其变成局部变量isQuit?
//2.isQuit = true;和printf("让t线程结束")谁先写?
public class ThreadDemo12 {
private static boolean isQuit = false;
public static void main(String[] args) {
//1.
//当我将全局变量变成局部变量的时候,就需要加上 final
//因为lamba表达式的变量捕获
//boolean isQuit = false;
//当我们加上final,外部的修改又不能运行
//因为被 final 修饰的局部变量,在初始化后不能再次赋值
//final boolean isQuit = false;
Thread t = new Thread(() ->{
while (!isQuit){
System.out.println("我是一个线程,工作中!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程工作完毕!");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//System.out.println("让t线程结束");
//isQuit = true;
//2.
//如果是修改成这样的话,main先打印还是t结束先打印就不知道了
isQuit = true;
System.out.println("让t线程结束");
}
}
方法二:使用Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
进行提前唤醒的操作
package thread;
//对12的优化写法
public class ThreadDemo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("我是一个线程,正在工作中!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
//e.printStackTrace();
break;
}
}
System.out.println("线程执行完毕");
});
t.start();
Thread.sleep(3000);
//使用 interrupt 方法,先修改刚才标志位的值
//修改为true
System.out.println("让 t 线程结束");
t.interrupt();
}
}
4.catch写法不同,为什么输出结果不同?
5.等待线程的执行顺序是怎么样的?
package thread;
import java.util.Random;
public class ThreadDemo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
Random random = new Random();
int n = random.nextInt();
for (int i = 0; i < n; i++) {
System.out.println("我是一个线程,正在工作中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程执行结束");
});
t.start();
//这个操作就是线程等待
//让 main线程 等 t线程结束
t.join();
System.out.println("这是主线程,期望这个日志在 t 结束后打印");
}
}
三、线程安全(重点)
1.为什么要学习线程安全?
2.怎么利用 synchronized 来加锁?
利用关键字来进行加锁操作
3.死锁是啥?怎么解决
官方
加锁是能解决线程安全问题,但是如果加锁方式不当,就可能产生死锁!!
死锁是多线程编程里会出现的一个问题。简单来说,当两个或多个线程都在等待对方释放锁,从而没办法继续执行下去时,就出现了死锁。
自身理解
小明和小红一起吃饺子,小明手里拿着酱油,小红手里拿着醋 。小明想要醋,小红想要酱油,但两人都不愿先把自己手里的调料给对方,都等着对方先给,结果谁都没法按自己的意愿吃饺子,这就如同编程里的死锁,两个线程都在等对方释放资源,导致程序无法继续运行 。
发生死锁的必要条件
- 互斥使用:锁就像单人马桶,一个人用着,其他人必须等,不能一起用。
- 不可抢占:好比借了别人的书,只有借书人主动还书,别人不能直接抢过来。锁也是,拿到锁的线程不主动释放,别人抢不走。
- 请求保持:比如你左手拿了苹果,还想右手拿香蕉,不放下苹果就去拿香蕉。线程拿了一个锁 A,还想拿另一个锁 B,且不放下 A。
- 循环等待:两人互相等着对方给东西。比如 A 等 B 的锁,B 等 A 的锁,形成 “死循环”。
解决死锁的核心:破坏这四个条件中的任意一个,死锁就不会发生啦!
典型死锁场景
1.一个线程,一把锁。(这个锁没有被synchronized修饰,不具备可以重复锁的功能)
2.两个线程,两把锁。线程1获取到锁A,线程2获取到锁B,接下来,1尝试获取B,2尝试获取A。
3.N个线程M把锁
把场景想象成一场大型派对,有 N 位客人(代表 N 个线程),派对上有 M 个热门游戏项目(代表 M 把锁),每个游戏同一时间只能容纳一人参与。
客人们各自先选择了一个游戏开始玩,之后他们又想去玩其他客人正在玩的游戏。但大家都不愿意先结束自己正在玩的游戏,于是所有人都卡在那里,既不能继续玩新游戏,也不愿意放弃当前游戏,派对的欢乐节奏就停滞了,对应到编程里就是出现了死锁。
破坏死锁的简单方法
-
避免 “请求保持”:
- 例子:吃饺子时,一次拿够醋、酱油等所有需要的调料,别拿了醋又去拿酱油。
- 编程:线程获取锁时,一次性拿完所有需要的锁,不分开拿。
-
打破 “循环等待”:
- 例子:给调料定顺序(如先拿醋,再拿酱油),大家都按这顺序拿,就不会出现 “你等我、我等你” 的循环。
- 编程:给锁编号,强制线程按固定顺序获取锁,避免循环等待。
-
允许 “锁被抢占”:
- 例子:设定拿调料的 “限时”,超时还没拿到全部调料,就放下已拿的,让别人先拿。
- 编程:给锁设置获取超时机制,若超时,线程主动释放已持有的锁,让其他线程有机会获取。
死锁代码 + 解决死锁的代码
package thread;
public class ThreadDemo22 {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() ->{
synchronized (A){
//sleep一下,给 t2 也能拿到 B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//尝试获取 B,并没有释放 A
synchronized (B){
System.out.println("t1 拿到了两把锁");
}
}
});
Thread t2 = new Thread(() ->{
//B -> A 解决加锁的问题
//如果保持上面是 B,下面是 A,跟t1一样就会导致 死锁 !!!
synchronized (A){
//sleep一下,给 t1 也能拿到 A
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//尝试获取 A,并没有释放 B
//A -> B 解决加锁的问题
synchronized (B){
System.out.println("t2 拿到了两把锁");
}
}
});
t1.start();
t2.start();
}
}
4.内存可见性引起的线程安全问题
概念
如果一个线程写,一个线程读,这个时候是否会有线程安全问题呢??
也是可能存在的!!
例子
当我们实际输入非0的值的时候
发现t1并没有真的结束!!!
因此当下这个情况也是 bug!!!
官方解释
- load 操作结果不变:每次执行 load(类似 “获取数据”),结果都一样。比如等用户输入时,程序在几秒内循环执行了上百亿次,可这期间数据根本没变化。
- load 操作开销大:load 操作比 “条件跳转”(判断下一步做什么)更耗资源,因为访问寄存器(高速存储区)比访问内存快很多,频繁用高消耗的 load 很不划算。
自身解释
想象你等烧水,水壶要 5 分钟才能开。但你每隔 1 秒就凑过去看 “水开了没”(这就是 load 操作),每次看水都没开(结果不变)。而且 “凑过去看” 这个动作(load 操作)比单纯在心里判断 “水开没开”(条件跳转)更麻烦、更费精力。这就像文段里说的:频繁做高消耗的 load 操作,结果还不变,纯属浪费。
解决代码
package thread;
import java.util.Scanner;
public class ThreadDemo23 {
//1.2
//private static int flag = 0;
//3.增加volatile关键字
//强制读写内存. 速度是慢了, 但是数据变的更准确了
private volatile static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
//1.吃了 JVM 的优化配置 -- 一直运行
// while (flag == 0){
// //循环啥的不写
// }
//2.不吃,因为睡眠了 -- 能出结果
// while (flag == 0){
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// }
while (flag == 0){
//循环啥的不写
}
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();
}
}
5.饿汉模式和懒汉模式的差别
图展示
自身理解
- 饿汉式:
餐厅提前把所有菜品都做好(不管有没有顾客点),比如把 100 道菜全做完摆桌上。虽然顾客来后能直接吃,但准备过程耗时久,还可能浪费(如果菜没卖完)。 - 懒汉式:
餐厅等顾客点菜后,再一道一道做。比如顾客先点 1 道菜,厨房马上做这 1 道;顾客再加点菜,厨房继续做新点的。这样不会提前浪费精力备菜,但每次点菜后需要等一会儿菜才能上桌。
总结:
- 饿汉式是 “提前准备好所有”,适合资源占用不高、需要快速使用的场景。
- 懒汉式是 “用到再准备”,适合资源庞大、按需使用的场景,避免一开始浪费过多资源。
6.饿汉模式和懒汉模式谁是线程安全
饿汉模式(是天然线程安全)
饿汉模式(程序启动,类加载的时候,就创建实例)
天然就是线程安全.只是涉及到读操作。
懒汉模式(不是天然)
首次调用的时候,涉及到读和写.就可能存在线程安全问题.=>创建出多个实例
1)加锁.把if和new包裹到一起
2)在锁外头加上条件.首次调用才加锁,后续调用不必加锁了.
3)指令重排序带来的问题.volatile
new操作可能涉及到三个步骤
a)申请内存
b)调用构造方法正常abc来执行
c)把内存地址赋值给引用可能被优化成acb
1. 初始懒汉式(线程不安全)
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {}
}
问题:多线程下,多个线程同时检测到 instance == null
,会创建多个实例,破坏单例。
类比:奶茶店没排队规则,多人同时买奶茶,店员给每个人都做一杯,最后每人拿到不同的奶茶。
2. 加锁版本(解决安全,但效率低)
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) { // 加锁
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy() {}
}
问题:每次调用 getInstance
都要加锁,即使实例已创建。就像奶茶店不管有没有人,所有人都必须排队,浪费时间。
3. 双重检查 + volatile(最终线程安全)
class SingletonLazy {
private volatile static SingletonLazy instance = null; // 加 volatile
private static final Object locker = new Object();
public static SingletonLazy getInstance() {
if (instance == null) { // 第一次检查
synchronized (locker) {
if (instance == null) { // 第二次检查
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
- 双重检查:先判断
instance
是否为空,不为空直接返回,减少加锁次数。 - volatile:禁止指令重排,确保
instance
初始化完成后再赋值,避免其他线程拿到未初始化的对象。
类比:奶茶店设置 “先看是否有做好的奶茶(第一次检查),没有再排队(加锁),排队时再确认一次(第二次检查)”,同时确保奶茶制作流程正确,不会 “半成品” 就给顾客。
7.饿汉模式和懒汉模式的使用场景
- 懒汉模式适用场景:
-
1. 资源紧张场景
描述:当某样东西平时不常用,但准备起来很麻烦 / 占地方时,等需要时再准备更划算。
例子:家庭露营装备(帐篷、睡袋等):平时放在仓库占地方,且组装耗时。只有计划露营时才拿出来清洗、组装,避免长期占用客厅空间。 -
2. 实例使用不确定场景
描述:某样东西可能永远用不上,但准备起来要花钱 / 费精力,不如等需要时再准备。
例子:家庭备用药品(如退烧药、烫伤膏):平时不一定生病,但每次购买都要花钱且有保质期。需要时再去药店买,避免过期浪费。 -
3. 延迟加载需求场景
描述:想灵活控制某件事的启动时机,按需触发。
例子:按需订阅的流媒体会员(如 Netflix):想看电影时才开通会员,而不是提前付费全年。用几天就暂停,灵活控制成本。
-
- 饿汉模式适用场景:
-
1. 资源占用少场景
描述:某样东西很便宜 / 不占地方,提前准备好反而更方便。
例子:家里的牙刷、牙膏:价格低且体积小,每天必用。直接买好放在卫生间,随时能用,无需临时购买。 -
2. 追求快速启动场景
描述:某件事需要快速响应,提前准备能节省时间。
例子:早餐店的包子:开店前就蒸好一批包子,顾客一来就能直接拿,避免现做等待,提升效率。 -
3. 高并发且线程安全场景
描述:多人同时需要某样东西,必须保证公平且不出错。
例子:医院的挂号系统:提前分配好所有诊室和医生的排班表(相当于提前创建实例),患者挂号时直接按规则分配,避免冲突。
-
一句话理解:
- 懒汉模式像 “外卖”:饿了才点,不囤货。
- 饿汉模式像 “冰箱里的泡面”:提前准备好,随时能吃。
四、锁机制类
-
synchronized
的原理是什么?-
答案:
synchronized
是基于Java对象头中的monitor(监视器锁)实现的,每个对象都有一个monitor,当线程进入synchronized
代码块或方法时,会尝试获取对象的monitor,获取成功则持有该monitor,否则会阻塞等待。 -
生活例子:
synchronized
的原理就像一个会议室的钥匙,只有拿到钥匙的人才能进入开会,其他人要在外面等钥匙被归还。
-
-
ReentrantLock
和synchronized
的区别是什么?-
答案:
ReentrantLock
提供了更灵活的锁操作,如尝试非阻塞获取锁、可中断获取锁、绑定多个条件等。ReentrantLock
需要手动释放锁,而synchronized
在方法执行完或代码块执行完后自动释放锁。 -
生活例子:
ReentrantLock
就像一个可以预约的会议室,你可以提前预订,也可以随时取消预订。而synchronized
就像一个只能在需要时临时进入的会议室,用完自动释放。
-
-
什么是锁的可重入性?
-
答案:锁的可重入性是指一个线程在获取到锁后,可以再次获取该锁而不会被阻塞。
-
生活例子:锁的可重入性就像一个人已经进入了会议室开会,如果需要再次进入同一个会议室(比如去拿资料),可以直接进入而不需要再次等待。
-
五、volatile关键字
volatile 和 synchronized 的区别
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final 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.count);
}
什么情况下使用volatile
流程图
文字描述
代码
package thread;
//就期望这个类只能有唯一的实例(一个进程中)
class SingletonLasy{
//private static SingletonLasy instance = null;
//加入volatile关键字,保证只能运行 123顺序
//1.每次访问必须重新读取内存(我后台更新了东西,前台刷新一下就行)
//2.禁止指令重新排列
private volatile static SingletonLasy instance = null;
private static Object locker = new Object();
public static SingletonLasy getInstance(){
if(instance == null){
synchronized (locker) {
if(instance == null) {
instance = new SingletonLasy();
}
}
}
return instance;
}
private SingletonLasy(){}
}
public class ThreadDemo27 {
public static void main(String[] args) {
//调用静态方法getInstance,不需要使用new的
SingletonLasy s1 = SingletonLasy.getInstance();
SingletonLasy s2 = SingletonLasy.getInstance();
System.out.println(s1 == s2);
}
}
自身理解
场景:餐厅打饭(单例对象 = 一份饭)。
- 正常流程:取碗(申请内存)→ 打饭(初始化)→ 端走(赋值给变量)。
- 指令重排序问题:
- 顾客 A:取碗(1)→ 先端着空碗给窗口看(3,赋值未初始化的对象)→ 还没打饭(2,未初始化),就被朋友叫走。
- 顾客 B:看到窗口有碗(以为有饭),直接端走空碗。结果吃的时候发现碗里没饭,出问题。
总结:步骤顺序错乱(指令重排序),导致后续使用 “半成品” 资源(未初始化的对象),引发错误。
六、wait
wait 和 sleep 的对比(重点)
相同点
- 线程执行控制:都能让线程放弃执行一段时间,实现线程的 “暂停” 状态。
- 时间相关特性:均支持设定时间,时间到达后线程解除阻塞,继续执行。
- 可提前唤醒:两者都能在设定时间未到时被提前唤醒,打破等待状态。
不同点
-
功能用途
- wait:核心用于线程间通信,等待其他线程通过
notify
/notifyAll
通知(不要求通知线程执行完)。 - sleep:单纯让线程阻塞一段时间,无通信目的;
join
虽也涉及线程等待,但其逻辑是 “等待另一线程执行完才继续”。
- wait:核心用于线程间通信,等待其他线程通过
-
唤醒机制
- wait:依赖其他线程调用
notify
/notifyAll
唤醒。 - sleep:通过
interrupt
方法唤醒,且这种唤醒通常意味着程序出现特殊异常情况,非常规业务流程。
- wait:依赖其他线程调用
-
使用场景逻辑
- wait:适用于 “不知道要等多久” 的场景,超时时间仅作为兜底方案。
- sleep:用于明确知道等待时间的场景,期望按设定时间准时唤醒。
-
语法与搭配
- 归属类:
wait
是Object
类的方法;sleep
是Thread
类的静态方法。 - 搭配使用:
wait
需搭配synchronized
关键字(在同步代码块中使用);sleep
无此要求,可直接调用。
- 归属类:
七、线程池
为什么使用线程池
使用多进程确实能够进行并发编程,但是频繁创建销毁进程,成本比较高
引入了线程(轻量级进程).复用资源的方式,来提高了创建销毁效率
随着创建销毁线程的频率进一步提升,开销仍然无法忽略不计了,那么就有两个解决方案
1)协程/纤程(轻量级线程)
2) 线程池提前把要使用的线程,在线程池中准备好.需要用就从池子里取,用完之后也是还给池子
自身理解
一、开了一家餐厅:
-
服务员团队 = 线程池里的线程
- 你提前雇佣了 5 个全职服务员(核心线程)
- 最多可以再临时招聘 3 个兼职(最大线程数)
-
顾客订单 = 需要处理的任务
- 当顾客进店,服务员会立即接待(直接分配线程处理)
- 高峰期订单太多时,顾客需要在候餐区排队(任务进入队列)
- 超过最大服务员数量时,就会提示 "本店已满"(拒绝策略)
-
服务员的工作流程:
- 空闲时在休息区待命(线程处于等待状态)
- 接到任务后立即服务(线程开始执行任务)
- 服务结束后回到休息区(线程归还线程池)
二、线程池的核心优势
-
节省资源:
- 不用每次来顾客都临时招聘和解雇服务员(避免频繁创建销毁线程)
-
高效响应:
- 服务员随时待命,顾客不用等待招聘时间(任务立即执行)
-
流量控制:
- 控制服务员数量避免混乱(防止过多线程耗尽资源)
三、现实中的应用场景
- 银行柜台:固定窗口数量,顾客取号排队
- 快递分拣中心:传送带相当于任务队列,工人相当于线程
- 外卖平台:骑手团队实时接单,高峰期动态调度
四、总结
线程池就像一支训练有素的服务团队:
- 预先准备好固定数量的线程(核心线程)
- 任务多时动态扩展线程(最大线程数)
- 任务少时自动收缩线程(节省资源)
- 通过队列管理等待处理的任务
线程池的核心参数有哪些?
答案:线程池的核心参数包括corePoolSizemaximumPoolSizekeepAliveTime
unit,workQueuethreadFactory 和 handler 等.
生活例子:线程池的核心参数就像餐厅的运营参数,corePoolSize是固定服务员的数量
maximumPoolSize 是最多能临时招聘的服务员数量keepAliveTime是临时服务员在没有
任务时能等待的时间,workQueue:是顾客的订单排队区域threadFactory是招聘服务员
的方式,handler:是当餐厅太忙无法接待更多顾客时的处理方式。
什么是CPU/IO密集型的任务
1. CPU 密集型任务
定义:任务大部分时间都在 “占用 CPU 做计算”,比如复杂的数学运算、数据处理。就像一个人一直闷头算数学题,离不开大脑(CPU)。
生活实例:
想象你开了一家饺子店,有个员工专门负责 “快速擀饺子皮”。他一直不停地揉面、擀皮(相当于 CPU 持续计算),几乎没有休息时间,所有精力都花在 “擀皮” 这个核心操作上。这种一直占用 “劳动力核心” 的工作,就是 CPU 密集型任务。
2. IO 密集型任务
定义:任务大部分时间在 “等待外部操作完成”,比如读取用户输入、读写文件、网络请求。CPU 在这段时间是空闲的,就像等人送货,等待时啥也不用干。
生活实例:
还是那家饺子店,另一个员工负责 “等快递送面粉”。他大部分时间都在刷手机等快递(等待 IO 操作),真正干活(用 CPU)的时间很少 —— 只有面粉送到后,搬面粉进店那一会儿需要动动手。这种 “大部分时间在等待,不咋占用核心劳动力” 的工作,就是 IO 密集型任务。
总结对比
类型 | 核心特点 | 生活类比 |
---|---|---|
CPU 密集型 | 疯狂占用 CPU 计算 | 员工一直擀皮,停不下来 |
IO 密集型 | 大部分时间在等待外部操作完成 | 员工等快递,闲着没事干 |
如何设定线程的数量
1. CPU 密集型任务的线程数量策略
文字含义:
如果一个进程里,所有线程都在疯狂占用 CPU 计算(比如算数学题、处理数据),这时线程数量不该超过 CPU 的逻辑核心数。
类比生活:
想象你有一家手工月饼店,只有 5 台烤箱(相当于 CPU 逻辑核心数)。每个月饼制作(线程)都需要一直占用烤箱烘烤(CPU 计算)。如果同时做 6 个月饼,烤箱不够用,第 6 个月饼只能等着,反而拖慢整体效率。所以,做这种 “一直占用核心资源” 的任务,数量不能超过核心资源数。
2. IO 密集型任务的线程数量策略
文字含义:
如果一个进程里,所有线程大部分时间在等待 IO(比如等用户输入、等文件读取),不咋占用 CPU,这时线程数量可以远超过 CPU 逻辑核心数。
类比生活:
还是那家月饼店,现在服务员负责 “等顾客下单”(IO 等待)。顾客没来时,服务员闲着(不占用烤箱 / CPU)。这时候可以多招服务员(线程),比如招 20 个。因为大部分时间他们在闲着等顾客(IO 等待),不会让烤箱(CPU)忙不过来。即使人多,也不影响核心资源,还能更快服务顾客。
总结对比
任务类型 | 线程数量策略 | 核心逻辑 | 生活类比总结 |
---|---|---|---|
CPU 密集型 | 不超过 CPU 逻辑核心数 | 避免 CPU 过度竞争,效率降低 | 烤箱有限,月饼多了挤着烤 |
IO 密集型 | 可远超过 CPU 逻辑核心数 | 线程多数时间空闲,不占 CPU | 服务员多等顾客,不占烤箱 |