为什么需要学习和使用多线程
一个简单的例子,在实现一些耗时的任务的时候(例如执行耗时5s的方法5次),如果让单线程去执行,就需要5*5 = 25s ,如果让5个线程去并发执行任务,只需要5秒的时间
基础知识
并行和并发
操作系统中的任务调度器会把CPU时间片分给不同的程序进行使用。
- 单核情况下,CPU会在线程之间快速切换,微观串行,宏观并行,线程会轮流使用CPU,相当于一个人在一个时刻只能做一件事,这种情况称为并发
- 多核情况下,CPU会同时执行多个任务,相当于多个人同时在做多件事,这种情况称为并行
Java线程
创建线程的方式
1.new Thread
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.setName("t1");
t1.start();
log.debug("running");
2.使用 Runnable 配合 Thread
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
3.FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// get方法会让主线程阻塞,同步等待 task 执行完毕的结果
Thread.sleep(1000);
Integer result = task3.get();
log.debug("结果是:{}", result);
log.info("测试是否同步");
4.小结
- 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
栈帧
线程每次执行到一个方法,都会生成一个栈帧,执行完方法后就会释放掉,大致流程如下图所示
启动线程的方式
1.run()
代码
@Slf4j
public class Test3 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
TimeUnit.SECONDS.sleep(5);
log.debug(Thread.currentThread().getName());
}
};
t1.run();
log.debug("do other things ...");
}
}
运行结果
可以看到需要等待run()方法体里面的内容执行完才可以往下执行,并且执行run()的是main线程
12:51:47.369 [main] DEBUG test.Test3 - main
12:51:47.371 [main] DEBUG test.Test3 - do other things ...
2.start()
代码
@Slf4j
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
TimeUnit.SECONDS.sleep(5);
log.debug(Thread.currentThread().getName());
}
};
t1.start();
log.debug("do other things ...");
}
}
运行结果 可以看到执行run()的是t1线程
12:50:17.239 [main] DEBUG test.Test4 - do other things ...
12:50:22.246 [t1] DEBUG test.Test4 - t1
3.总结
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会 提示(hint) 调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
代码
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (; ; ) {
//Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
运行结果 启动代码,稍作等待,然后停止,可以看到线程2的值增长的比线程1的大,证明了优先级是有作用的
- 第一次运行的结果
---->1 212101
---->1 212102
---->1 212103
---->1 212104
---->1 212105
---->2 256924
---->2 256925
---->2 256926
---->2 256927
---->2 256928
- 第二次运行的结果
---->1 108961
---->1 108962
---->1 108963
---->1 108964
---->2 186360
---->2 186361
---->2 186362
---->2 186363
---->2 186364
---->2 186365
join 方法详解
join用于解决需要等待异步线程的返回结果
单线程的join
首先查看这段代码,我们预期的r应该是10,但是实际得到的结果为0。因为主线程不会等待t1线程执行完,就直接输出了r,所以控制台输出的r为0
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
//t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
运行效果
16:35:14.438 [main] DEBUG test.Test10Join - 开始
16:35:14.478 [Thread-0] DEBUG test.Test10Join - 开始
16:35:14.479 [Thread-0] DEBUG test.Test10Join - 结束
16:35:14.478 [main] DEBUG test.Test10Join - 结果为:0
16:35:14.479 [main] DEBUG test.Test10Join - 结束
那么应该如何修改呢?
这个时候就可以用t1.join();,这个方法会让主线程等待t1线程执行完再往下走,取消掉//t1.join();的注释,运行代码,可以看到已经能够拿到r的值了。通过观察时间也可以观察到确实是主线程会等待t1线程执行完
16:49:53.152 [main] DEBUG test.Test10Join - 开始
16:49:53.193 [Thread-0] DEBUG test.Test10Join - 开始
16:49:53.195 [Thread-0] DEBUG test.Test10Join - 结束
16:49:53.195 [main] DEBUG test.Test10Join - 结果为:10
16:49:53.196 [main] DEBUG test.Test10Join - 结束
多线程的join
对于多线程来说,join的结果是以执行时间最长的线程为主。例如下面的例子,无论是t1.join()在前还是在后,运行的花费的时间都是2s,因为要等待最长时间的那个线程执行完毕
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
运行结果
17:01:43.026 [main] DEBUG test.Test11Join - r1: 10 r2: 20 cost: 2003
有时效的 join
- 顾名思义,就是在join的时候会给这个线程一个最长等待时间,如果超过最长等待时间还没等到任务执行完成,那么就会放弃等待这个线程,直接往下执行。
- 如果在这个最长等待时间之内完成任务就可以正常执行
- 如果在最长等待时间之内都没有完成任务,正如下面的代码,等待
t1执行1500ms,但是t1执行完需要2000ms,所以主线程就不会拿到r1 = 10的值,因此输出的结果为0
代码
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
运行结果
17:08:31.335 [main] DEBUG test.Test12Join - r1: 0 r2: 0 cost: 1513
interrupt 方法详解
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态 打断 sleep 的线程, 会清空打断状态,以 sleep 为例
代码
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
// 打断状态: {}false
log.info(" 打断状态: {}" , t1.isInterrupted());
}
运行结果
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at test.Test13Interrupt.lambda$main$0(Test13Interrupt.java:14)
at java.lang.Thread.run(Thread.java:748)
17:21:16.723 [main] INFO test.Test13Interrupt - 打断状态: false
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
代码
public static void main(String[] args) throws Exception {
Thread t2 = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
运行结果
打断状态: {}true
打断park线程
- 打断 park 线程, 不会清空打断状态
代码
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unPark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
运行结果
17:33:30.712 [t1] DEBUG test.Test15Interrupt - park...
17:33:31.722 [t1] DEBUG test.Test15Interrupt - unPark...
17:33:31.722 [t1] DEBUG test.Test15Interrupt - 打断状态:true
- 如果打断标记已经是 true, 则park会失效
可以看到当第一次park被打断之后,LockSupport.park()不再生效,会继续执行下面的输出代码,证明了上面的park会失效
代码
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
运行结果
17:36:45.832 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.836 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
Process finished with exit code 0
主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
代码
public static void main(String[] args) throws InterruptedException {
log.debug("开始运行1...");
Thread t1 = new Thread(() -> {
log.debug("开始运行2...");
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束2...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1000);
log.debug("运行结束1...");
}
运行结果
17:46:08.213 [main] DEBUG daemon.TestDaemon - 开始运行1...
17:46:08.250 [daemon] DEBUG daemon.TestDaemon - 开始运行2...
17:46:09.256 [main] DEBUG daemon.TestDaemon - 运行结束1...
线程状态
操作系统层面
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从 【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
Java API层面
根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
- TERMINATED 当线程代码运行结束
通过代码来解析java中线程不同的状态
代码
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Test01.class) {
try {
TimeUnit.SECONDS.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Test01.class) {
try {
TimeUnit.SECONDS.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
Thread.sleep(1000);
System.out.println("t1.getState() = " + t1.getState());
System.out.println("t2.getState() = " + t2.getState());
System.out.println("t3.getState() = " + t3.getState());
System.out.println("t4.getState() = " + t4.getState());
System.out.println("t5.getState() = " + t5.getState());
System.out.println("t6.getState() = " + t6.getState());
}
}
运行结果
18:07:04.821 [t3] DEBUG state.Test01 - running
t1.getState() = NEW
t2.getState() = RUNNABLE
t3.getState() = TERMINATED
t4.getState() = TIMED_WAITING
t5.getState() = WAITING
t6.getState() = BLOCKED
共享模型之管程
本章内容
- 共享问题
- synchronized
- 线程安全分析
- Monitor
- wait/notify
- 线程状态转换
- 活跃性
- Lock
共享带来的问题
截取例子
Java 的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
代码
public class Test {
static int counter = 0;
//static修饰,则元素是属于类本身的,不属于对象,与类一起加载一次,只有一个
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
//synchronized (room) {
counter++;
//}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
//synchronized (room) {
counter--;
//}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
运行结果
// 第一次
-586
// 第二次
-1590
// 第三次
369
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析
- 对于
i++
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
- 对于
i--
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
1.如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
2.但多线程下这 8 行代码可能交错运行:
- 出现负数的情况:
- 出现正数的情况:
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment() // 临界区
{
counter++;
}
static void decrement() // 临界区
{
counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
对于:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,解决方案如下,加上synchronize,只有获得了room才可以执行counter--。另外一个没有获得room的线程就需要等待
代码
static int counter = 0;
//static修饰,则元素是属于类本身的,不属于对象,与类一起加载一次,只有一个
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
运行结果
// 每次都是0
0
你可以做这样的类比:
synchronized(对象)中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人- 当线程 t1 执行到
synchronized(room)时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++代码 - 这时候如果 t2 也运行到了
synchronized(room)时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
- 当 t1 执行完
synchronized{}块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的count--代码
用图来表示
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。
面向对象改进
把需要保护的共享变量放入一个类
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
然后进行改进
代码
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}", room.get());
}
}
运行结果
22:21:17.001 [main] DEBUG syn.Test01 - count: 0
方法上的 synchronized
- 普通方法上的锁其实锁的是this,也就是当前类的实例对象
class Test{
public synchronized void test() {
}
}
// 等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
- 静态方法上的锁的是Class
class Test{
public synchronized static void test() {
}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象
情况1:锁的是普通方法,相当于锁住this,有可能出现12 或者 21
public class TestSyn01 {
public static void main(String[] args) {
/*
* 锁住的是Number类的对象,也就是n1
*/
Number n1 = new Number();
new Thread(n1::a).start();
new Thread(n1::b).start();
}
}
@Slf4j
class Number {
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
情况2:1s1再2 或者2 1s后1
public class TestSyn02 {
public static void main(String[] args) {
Number01 n1 = new Number01();
new Thread(n1::a).start();
new Thread(n1::b).start();
}
}
@Slf4j(topic = "c.Number")
class Number01 {
public synchronized void a() {
try {
sleep(1000);
log.debug("1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b() {
log.debug("2");
}
}
情况3:3 1s1 2 或者3 2 1s1 或者23 1s1
public class TestSyn03 {
public static void main(String[] args) {
Number03 n1 = new Number03();
new Thread(n1::a).start();
new Thread(n1::b).start();
new Thread(n1::c).start();
}
}
@Slf4j
class Number03 {
public synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
情况4:2 1s1(锁的不是同一个对象,互不影响)
public class TestSyn04 {
public static void main(String[] args) {
Number04 n1 = new Number04();
Number04 n2 = new Number04();
new Thread(n2::b).start();
new Thread(n1::a).start();
}
}
@Slf4j(topic = "c.Number")
class Number04 {
public synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
情况5:2 1s1 (锁的不是同一个东西,一个锁的是this,一个是Class)
public class TestSyn05 {
public static void main(String[] args) {
Number05 n1 = new Number05();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number05 {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
情况6:1s1 2 或者 2 1s1(锁的都是Class)
public class TestSyn06 {
public static void main(String[] args) {
Number06 n1 = new Number06();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number06 {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
情况7: 2 1s1 (锁的不是同一个东西)
public class TestSyn07 {
public static void main(String[] args) {
Number07 n1 = new Number07();
Number07 n2 = new Number07();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
}
@Slf4j(topic = "c.Number")
class Number07{
@SneakyThrows
public static synchronized void a() {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
情况8: 2 1s 1 或者 1s1 2(锁的是Class)
public class TestSyn08 {
public static void main(String[] args) {
Number08 n1 = new Number08();
Number08 n2 = new Number08();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
}
@Slf4j
class Number08 {
public static synchronized void a() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
Monitor 原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
注意 synchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器,不遵从以上规则
变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
如图
局部变量的引用稍有不同
先看一个成员变量的例子:同时操作一个在外部的list,可能导致出现ArrayIndexOutOfBoundsException,因为可能list没有放元素进去就调用remove了
栈帧示意图
代码
public class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
运行结果
Exception in thread "Thread0" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.ArrayList.add(ArrayList.java:459)
at safe.ThreadUnsafe.method2(ThreadUnsafe.java:34)
at safe.ThreadUnsafe.method1(ThreadUnsafe.java:27)
at safe.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:19)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.ArrayList.remove(ArrayList.java:501)
at safe.ThreadUnsafe.method3(ThreadUnsafe.java:38)
at safe.ThreadUnsafe.method1(ThreadUnsafe.java:28)
at safe.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:19)
at java.lang.Thread.run(Thread.java:748)
对上面的例子进行改进
把ArrayList<String> list = new ArrayList<>();放到method1()里面进行初始化,这样无论是哪个线程进来都拥有了属于自己的list,然后list再调用method2和method3,同步执行代码就不会报错了,也就是先执行完2再执行3
栈帧示意图
代码
public class ThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
//ThreadSafe test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
运行结果
// 无论运行多少次,都不会报错
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
- 情况1:有其它线程调用 method2 和 method3
- 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
问题所在
正常流程是调用完了add然后再调用remove,但是如果调用的是new Thread可能会导致remove方法在add之前调用,导致出现IndexOutOfBoundsException异常
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
-
它们的每个方法是原子的
-
但注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable(); // 线程1,线程2
if( table.get("key") == null)
{
table.put("key", value);
}
通过观察下面这个图,可以看到这个是非线程安全的,最终v1把v2给干掉了
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
代码
- 首先定义一个字符串,调用substring方法,查看方法是如何实现的
String str = "abc";
String substring = str.substring(0, 1);
- 首先是做了一些判断,最终调用到
new String(value, beginIndex,subLen)方法
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
- 进入
new String(value, beginIndex,subLen)方法查看
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
- 方法这么长,但是用于确保线程安全的地方是
this.value = Arrays.copyOfRange(value, offset, offset+count),这个方直接拷贝一份原字符串,所以在多线程同时操作String类型的情况下,每个线程拿到的都是新拷贝出来的字符串,所以不会存在线程安全问题
卖票练习
代码
@Slf4j
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票
List<Integer> sellCount = new Vector<>();
// List<Integer> sellCount = new ArrayList<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
//synchronized
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
运行结果
// 正常情况
11:27:39.134 [main] DEBUG ticket.ExerciseSell - selled count:2000
11:27:39.137 [main] DEBUG ticket.ExerciseSell - remainder count:0
// 出现线程安全问题的情况
11:42:44.842 [main] DEBUG ticket.ExerciseSell - selled count:2004
11:42:44.845 [main] DEBUG ticket.ExerciseSell - remainder count:0
这里采用的是Vector,如果采用ArrayList呢?
运行结果如下,会出现异常
Exception in thread "main" java.lang.NullPointerException
at ticket.ExerciseSell.lambda$main$2(ExerciseSell.java:39)
at java.util.stream.ReferencePipeline$4$1.accept(ReferencePipeline.java:210)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.IntPipeline.reduce(IntPipeline.java:456)
at java.util.stream.IntPipeline.sum(IntPipeline.java:414)
at ticket.ExerciseSell.main(ExerciseSell.java:39)
尝试分析一下问题出现的原因
对比ArrayList和Vertor的add方法
- ArrayList - add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- Vector - add
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
可以看到主要的区别是在方法上面加了synchronized关键字,加在普通的方法上,锁的是this,对应上面的例子来说,this是sellCount。
用ArrayList去操作add可能会出现一个多个线程同时操作elementData[size++] = e,原本size为0,2个线程同时size++然后size就变成了2,然后同时给size 为 2的下标赋值,导致size为1的地方是null,所以抛出NPE
对于上述例子,正确的修改方式应该是改成下面这样:
- 用Vector代替ArrayList
- 给方法加上synchronized关键字,实现
ticketWindow的互斥修改
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票
List<Integer> sellCount = new Vector<>();
//List<Integer> sellCount = new ArrayList<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
转账练习
代码
@Slf4j
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void transfer(Account target, int amount) {
//synchronized (Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
//}
}
}
运行结果
// 结果可能大于2000
12:07:35.540 [main] DEBUG ticket.ExerciseTransfer - total:2782
12:14:11.472 [main] DEBUG ticket.ExerciseTransfer - total:4328
12:14:16.994 [main] DEBUG ticket.ExerciseTransfer - total:22524
// 结果可能小于2000
12:14:29.274 [main] DEBUG ticket.ExerciseTransfer - total:554
问题出现的原因是没有对共享变量同时进行保护,因此只需要加上Account.class的锁即可
小故事
故事角色
- 老王 - JVM
- 小南 - 线程
- 小女 - 线程
- 房间 - 对象
- 房间门上 - 防盗锁 - Monitor
- 房间门上 - 小南书包 - 轻量级锁
- 房间门上 - 刻上小南大名 - 偏向锁
- 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
- 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样, 即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女 晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因 此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是 自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那 么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦 掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老 家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老 王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
wait notify
小故事 - 为什么需要 wait
- 由于条件不满足,小南不能继续进行计算
- 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
- 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开, 其它人可以由老王随机安排进屋
- 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
- 小南于是可以离开休息室,重新进入竞争锁的队列
API 介绍
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
代码
@Slf4j
public class Test01 {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行1....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码1....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行2....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码2....");
}
}).start();
// 主线程两秒后执行
try {
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
//obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
运行结果
// notify()的运行结果 唤醒的不一定是1,也可能是2,随机事件
12:32:28.817 [Thread-0] DEBUG notify.Test01 - 执行1....
12:32:28.820 [Thread-1] DEBUG notify.Test01 - 执行2....
12:32:30.826 [main] DEBUG notify.Test01 - 唤醒 obj 上其它线程
12:32:30.826 [Thread-0] DEBUG notify.Test01 - 其它代码1....
// notifyAll()的运行结果
12:33:11.321 [Thread-0] DEBUG notify.Test01 - 执行1....
12:33:11.323 [Thread-1] DEBUG notify.Test01 - 执行2....
12:33:13.331 [main] DEBUG notify.Test01 - 唤醒 obj 上其它线程
12:33:13.331 [Thread-1] DEBUG notify.Test01 - 其它代码2....
12:33:13.331 [Thread-0] DEBUG notify.Test01 - 其它代码1....
wait()方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止wait(long n)有时限的等待, 到 n 毫秒后结束等待,或是被 notify
wait notify 的正确姿势
sleep(long n) 和wait(long n)的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们状态是 TIMED_WAITING
step 1
代码
@Slf4j
public class Test02 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
// 这里能不能加 synchronized (room)?
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟的").start();
}
}
运行结果
其他人会被小南阻塞住,无法执行任务
14:19:39.085 [小南] DEBUG notify.Test02 - 有烟没?[false]
14:19:39.087 [小南] DEBUG notify.Test02 - 没烟,先歇会!
14:19:40.096 [送烟的] DEBUG notify.Test02 - 烟到了噢!
14:19:41.092 [小南] DEBUG notify.Test02 - 有烟没?[true]
14:19:41.092 [小南] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
- 其它干活的线程,都要一直阻塞,效率太低
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
- 解决方法,使用 wait - notify 机制
step2
代码
@Slf4j
public class Test03 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("可以开始干活了");
}
}, "其它人").start();
}
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
}
运行结果
14:28:11.949 [小南] DEBUG notify.Test03 - 有烟没?[false]
14:28:11.952 [小南] DEBUG notify.Test03 - 没烟,先歇会!
14:28:13.955 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:15.963 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:17.977 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:19.986 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:21.995 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:21.995 [小南] DEBUG notify.Test03 - 有烟没?[false]
14:28:21.995 [送烟的] DEBUG notify.Test03 - 烟到了噢!
如果小南只等待2s,但是其他线程也需要等待,但是等送烟的来了之后已经超过2s了,会导致小南的任务无法继续执行
step3
代码
@Slf4j
public class Test04 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
运行结果
14:32:16.904 [小南] DEBUG notify.Test04 - 有烟没?[false]
14:32:16.907 [小南] DEBUG notify.Test04 - 没烟,先歇会!
14:32:16.908 [小女] DEBUG notify.Test04 - 外卖送到没?[false]
14:32:16.908 [小女] DEBUG notify.Test04 - 没外卖,先歇会!
14:32:17.908 [送外卖的] DEBUG notify.Test04 - 外卖到了噢!
14:32:17.908 [小女] DEBUG notify.Test04 - 外卖送到没?[true]
14:32:17.908 [小女] DEBUG notify.Test04 - 可以开始干活了
14:32:17.908 [小南] DEBUG notify.Test04 - 没烟,先歇会!
Park & Unpark
基本用法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
情况1
@Slf4j
public class Test01 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
try {
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
运行结果
14:35:47.340 [t1] DEBUG park.Test01 - start...
14:35:48.348 [t1] DEBUG park.Test01 - park...
14:35:49.344 [main] DEBUG park.Test01 - unpark...
14:35:49.344 [t1] DEBUG park.Test01 - resume...
47s park住,然后等了2s之后 执行unpark,t1才可以往下执行
情况2
@Slf4j
public class Test02 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
运行结果
14:45:10.847 [t1] DEBUG park.Test02 - start...
14:45:11.857 [main] DEBUG park.Test02 - unpark...
14:45:12.863 [t1] DEBUG park.Test02 - park...
14:45:12.863 [t1] DEBUG park.Test02 - resume...
问题:为什么调用了park会马上resume呢?
可以引入干粮这个概念
- 干粮默认为0
- 调用unpark会让干粮+1
- 调用park会检查干粮并且让干粮-1,如果干粮为1就继续执行,否则就park住
多把锁
多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低 解决方法是准备多个房间(多个对象锁)
例如
@Slf4j
public class Test01 {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j
class BigRoom {
@SneakyThrows
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
TimeUnit.SECONDS.sleep(2);
}
}
@SneakyThrows
public void study() {
synchronized (this) {
log.debug("study 1 小时");
TimeUnit.SECONDS.sleep(1);
}
}
}
运行结果
22:44:11.239 [小南] DEBUG lock.BigRoom - study 1 小时
22:44:12.249 [小女] DEBUG lock.BigRoom - sleeping 2 小时
在只有一把锁的情况下,需要灯带stydu结束之后才可以进行下一个任务
改进
public class Test02 {
public static void main(String[] args) {
BigRoom02 bigRoom = new BigRoom02();
new Thread(() -> {
bigRoom.sleep();
}, "小南").start();
new Thread(() -> {
bigRoom.study();
}, "小女").start();
}
}
@Slf4j
class BigRoom02 {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
@SneakyThrows
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
TimeUnit.SECONDS.sleep(2);
}
}
@SneakyThrows
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
TimeUnit.SECONDS.sleep(1);
}
}
}
运行结果
22:46:05.646 [小南] DEBUG lock.BigRoom02 - sleeping 2 小时
22:46:05.646 [小女] DEBUG lock.BigRoom02 - study 1 小时
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获得A对象锁,接下来想获取B对象的锁
t2线程获得B对象锁,接下来想获取A对象的锁
例子如下:
代码
@Slf4j
public class Test01 {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
try {
sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
运行结果
22:52:21.896 [t1] DEBUG multiple_lock.Test01 - lock A
22:52:21.896 [t2] DEBUG multiple_lock.Test01 - lock B
检测死锁的工具
jconsole
可以在CMD输入jconsole,然后选择正在执行的类,点击连接
然后点击线程,点击检测死锁
然后就可以看到发生死锁的线程详情了
jps
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待