多线程基础
# 多线程基础
# 1. 什么是并发与并行
# 1.1 并行(parallel) 多对多
指两个或多个事件在同一时刻
发生(同时发生)。
- 是在不同实体上的多个事件
- 是在多个处理器上
同时
处理多个任务 - 同一时刻,大家都在做事情,你做你的,我做我的,但是我们都在做。
示例:泡方便面
# 1.2 并发(concurrent) 一对多
指两个或多个事件在同一个时间段
内发生。
- 是在同一实体上的多个事件
- 是在同一台处理器上
同时
处理多个任务 - 同一时刻,其实是只有一个时间在发生
应用场景:抢票、秒杀(同一服务处理同一件事情)
# 2. 什么是进程、线程 😄
# 2.1 进程
进程是正在运行的程序的实例。
进程是线程的容器,即一个进程中可以开启多个线程。
比如打开一个浏览器、打开一个word等操作,都会创建进程。
简单的说,咋系统中运行的一个应用程序就是一个进程,每一个进程都有它自己的内存空间和系统资源。
# 2.2 线程:
线程是进程内部的一个独立执行单元;一个进程可以同时并发运行多个线程;
比如进程可以理解为医院,线程是挂号、就诊、缴费、拿药等业务活动
概述:线程也被称为轻量级进程
,在同一进程内会有一个或多个线程,是大多数操作系统进行时序调度的基本单元。
多线程:多个线程并发执行。
# 2.3 管程
提示
管程为 Monitor(监视器)
,也就是我们平时所说的锁。
Monitor其实是一种同步机制,它的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
JVM中同步是基于进入和退出监视器对象(Monitor:管程对象)来实现的。每个对象实例都会有一个Monitor对象。
// 管程 Monitor 示例
Object o = new Object();
new Thread(()->{
synchronized (o){
}
}, "aa").start();
2
3
4
5
6
7
8
9
Monitor对象会和Java对象一同创建并销毁,它的底层是由C++语言来实现的。
同步指令
# 3. 线程创建(4种) 😄
提示
Java中线程四种创建方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池
# 2.1. 继承Thread类
# 第一步:创建自定义线程类
/**
* @Author: CHGGX
* @Description: <h1> 继承Thread类 </h1>
*/
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println("MyThread执行了: "+ new Date().getTime());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 第二步:创建测试类
/**
* @Author: CHGGX
* @Description: <h1> 继承Thread类测试 </h1>
*/
public class ThreadCreateTest {
public static void main(String[] args) {
// 1. 创建自定义线程实例
MyThread myThread = new MyThread();
// 2. 启动线程
myThread.start();
// 3. 在main主线程打印信息
for (int i = 0; i < 10; i++) {
// System.currentTimeMillis() new Date().getTime()
System.out.println("main主线程执行了: "+ System.currentTimeMillis());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
执行效果如下:
点击查看
main主线程执行了: 1593331803478
main主线程执行了: 1593331803478
main主线程执行了: 1593331803478
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
main主线程执行了: 1593331803479
MyThread执行了: 1593331803479
MyThread执行了: 1593331803479
MyThread执行了: 1593331803479
MyThread执行了: 1593331803479
MyThread执行了: 1593331803479
MyThread执行了: 1593331803480
MyThread执行了: 1593331803480
MyThread执行了: 1593331803480
MyThread执行了: 1593331803480
MyThread执行了: 1593331803480
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.2. 实现Runnable接口
# 第一步:创建自定义类实现Runnable接口
/**
* @Author: CHGGX
* @Description: <h1> 实现Runnable接口 </h1>
*/
public class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行时间是: " + System.currentTimeMillis() + "; 执行次数是: " + i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 第二步:创建测试类
/**
* @Author: CHGGX
* @Description: <h1> 实现Runnable接口测试 </h1>
*/
public class ThreadCreateTest {
public static void main(String[] args) {
// 1. 创建自定义线程实例
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
// 2. 启动线程
thread.start();
// 3. 在main主线程打印信息
for (int i = 0; i < 10; i++) {
// System.currentTimeMillis() new Date().getTime()
System.out.println("main主线程执行了: "+ System.currentTimeMillis());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
执行效果如下:
点击查看
main主线程执行了: 1593332356779
Thread-0执行时间是: 1593332356779; 执行次数是: 0
main主线程执行了: 1593332356779
Thread-0执行时间是: 1593332356779; 执行次数是: 1
main主线程执行了: 1593332356779
Thread-0执行时间是: 1593332356779; 执行次数是: 2
main主线程执行了: 1593332356779
Thread-0执行时间是: 1593332356780; 执行次数是: 3
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 4
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 5
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 6
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 7
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 8
main主线程执行了: 1593332356780
Thread-0执行时间是: 1593332356780; 执行次数是: 9
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.3. 实现Callable接口
# FutureTask介绍
Callable需要使用FutureTask
类帮助执行,FutureTask
类结构如下:
Future接口:
判断任务是否完成:isDone()
能够中断任务:cancel()
能够获取任务执行结果:get()
# 第一步:创建自定义类实现Callable接口
/**
* @Author: CHGGX
* @Description: <h1> 实现Callable接口 </h1>
*/
public class MyCallable implements Callable<String> {
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行时间是: " + System.currentTimeMillis() + "; 执行次数是: " + i);
}
return "MyCallable接口执行完成";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 第二步:创建测试类
/**
* @Author: CHGGX
* @Description: <h1> 实现Callable接口测试 </h1>
*/
public class ThreadCreateTest {
public static void main(String[] args) {
// 实现Callable接口
// 1. 创建FutureTask实例,创建MyCallable实例
MyCallable callable = new MyCallable();
FutureTask<String> task = new FutureTask<String>(callable);
// 2. 创建Thread实例,执行FutureTask
Thread thread = new Thread(task, "MyCallable");
// 3. 启动线程
thread.start();
// 4. 在main主线程打印信息
for (int i = 0; i < 10; i++) {
System.out.println("main主线程执行了: " + System.currentTimeMillis());
}
// 5. 获取并打印MyCallable执行结果
try {
String result = task.get();
System.out.println("MyCallable执行结果是: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
运行效果图如下:
点击查看
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 0
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 1
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 2
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 3
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 4
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 5
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 6
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 7
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 8
main主线程执行了: 1593333236357
MyCallable执行时间是: 1593333236357; 执行次数是: 9
MyCallable执行结果是: MyCallable接口执行完成
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.4. 线程池-Executor
# 线程池线类关系图
- Executor接口:
声明了execute(Runnable runnable)方法,执行任务代码
- ExecutorService接口:
继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等
- AbstractExecutorService抽象类:
实现ExecutorService接口,基本实现ExecutorService中声明的所有方法
- ScheduledExecutorService接口:
继承ExecutorService接口,声明定时执行任务方法
- ThreadPoolExecutor类:
继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法
- ScheduledThreadPoolExecutor类:
继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法
Executors类
:
提供快速创建线程池的方法
# 第一步:创建自定义类实现Runnable接口
/**
* @Author: CHGGX
* @Description: <h1> 线程池-Executor </h1>
*/
public class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行时间是: " + System.currentTimeMillis() + "; 执行次数是: " + i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 第二步:创建测试类
/**
* @Author: CHGGX
* @Description: <h1> 线程池-Executor测试 </h1>
*/
public class ThreadCreateTest {
public static void main(String[] args) {
// 使用线程池创建线程
// 1. 使用Executors获取线程池对象
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 2. 通过线程池对象获取线程并执行MyRunnable实例
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
// 3. 在main主线程打印信息
for (int i = 0; i < 10; i++) {
System.out.println("main主线程执行了: "+ System.currentTimeMillis());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
运行效果图如下:
点击查看
main主线程执行了: 1593333911301
main主线程执行了: 1593333911301
main主线程执行了: 1593333911301
main主线程执行了: 1593333911301
main主线程执行了: 1593333911301
pool-1-thread-1执行时间是: 1593333911301; 执行次数是: 0
main主线程执行了: 1593333911301
pool-1-thread-1执行时间是: 1593333911301; 执行次数是: 1
main主线程执行了: 1593333911301
pool-1-thread-1执行时间是: 1593333911301; 执行次数是: 2
main主线程执行了: 1593333911302
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 3
main主线程执行了: 1593333911302
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 4
main主线程执行了: 1593333911302
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 5
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 6
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 7
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 8
pool-1-thread-1执行时间是: 1593333911302; 执行次数是: 9
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.5. 小结
# 实现接口和继承Thread类比较
接口更适合多个相同的程序代码的线程去共享同一个资源。
接口可以
避免java中的单继承的局限性
。接口代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类
。
扩充:
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
# Runnable和Callable接口比较
相同点:
两者都是接口;
两者都可用来编写多线程程序;
两者都需要调用Thread.start()启动线程;
不同点:
实现Callable接口的线程能
返回执行结果
;而实现Runnable接口的线程不能返回结果;Callable接口的call()方法允许
抛出异常
;而Runnable接口的run()方法的不允许抛异常;实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能
注意: Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取
将来
结果;当不调用此方法时,主线程不会阻塞!
# 3. 线程生命周期 😄
# 3.1. 新建
new关键字
创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值
# 3.2. 就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
JVM为线程创建方法栈和程序计数器,等待线程调度器调度
# 3.3. 运行
就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
# 3.4. 阻塞
当发生如下情 况时,线程将会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
线程试图获得一个同步锁(同步监视器),但该同步锁正被其他线程所持有。
线程在等待某个通知(notify)
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
# 3.5. 死亡
线程会以如下3种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束
。线程抛出一个未捕获的Exception或Error
。调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
# 4. 线程安全问题 😄
# 4.1. 什么是线程安全
如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。
# 4.2. 问题演示
为了演示线程安全问题,我们采用多线程模拟多个窗口同时售卖《孙悟空大战超人》电影票。
# 第一步:创建售票线程类
/**
* @Author: CHGGX
* @Description: <h1> 售票 </h1>
*/
public class Ticket implements Runnable {
/**
* 电影票数据量,默认100张
*/
private int ticketNum = 100;
public void run() {
while (true) {
// 判断是否有票
if (ticketNum > 0) {
try {
// 有票,让线程睡眠100ms
Thread.sleep(100);
// 打印当前售出的票数字和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "销售电影票: " + ticketNum--);
// 票数据-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 第二步:创建测试类
/**
* @Author: CHGGX
* @Description: <h1> </h1>
*/
public class TicketSaleMain {
public static void main(String[] args) {
// 1. 创建电影票对象
Ticket ticket = new Ticket();
// 2. 创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1");
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
// 3. 开启线程
thread.start();
thread2.start();
thread3.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
运行结果如下:
点击查看
线程窗口2销售电影票: 99
线程窗口1销售电影票: 100
线程窗口3销售电影票: 98
线程窗口1销售电影票: 97
线程窗口3销售电影票: 97
线程窗口2销售电影票: 97
线程窗口1销售电影票: 96
线程窗口2销售电影票: 96
线程窗口3销售电影票: 95
线程窗口3销售电影票: 94
线程窗口1销售电影票: 93
线程窗口2销售电影票: 92
线程窗口1销售电影票: 91
线程窗口3销售电影票: 90
线程窗口2销售电影票: 89
线程窗口3销售电影票: 88
线程窗口2销售电影票: 87
线程窗口1销售电影票: 86
线程窗口2销售电影票: 85
线程窗口3销售电影票: 83
线程窗口1销售电影票: 84
线程窗口3销售电影票: 82
线程窗口2销售电影票: 80
线程窗口1销售电影票: 81
线程窗口2销售电影票: 79
线程窗口1销售电影票: 78
线程窗口3销售电影票: 77
线程窗口1销售电影票: 76
线程窗口3销售电影票: 76
线程窗口2销售电影票: 76
线程窗口3销售电影票: 75
线程窗口2销售电影票: 74
线程窗口1销售电影票: 75
线程窗口2销售电影票: 73
线程窗口1销售电影票: 73
线程窗口3销售电影票: 73
线程窗口2销售电影票: 72
线程窗口3销售电影票: 72
线程窗口1销售电影票: 72
线程窗口1销售电影票: 71
线程窗口2销售电影票: 70
线程窗口3销售电影票: 71
线程窗口2销售电影票: 69
线程窗口3销售电影票: 68
线程窗口1销售电影票: 68
线程窗口1销售电影票: 67
线程窗口2销售电影票: 67
线程窗口3销售电影票: 67
线程窗口2销售电影票: 65
线程窗口3销售电影票: 64
线程窗口1销售电影票: 66
线程窗口1销售电影票: 63
线程窗口3销售电影票: 62
线程窗口2销售电影票: 63
线程窗口1销售电影票: 60
线程窗口2销售电影票: 61
线程窗口3销售电影票: 60
线程窗口2销售电影票: 59
线程窗口3销售电影票: 58
线程窗口1销售电影票: 57
线程窗口3销售电影票: 56
线程窗口1销售电影票: 55
线程窗口2销售电影票: 56
线程窗口2销售电影票: 54
线程窗口3销售电影票: 53
线程窗口1销售电影票: 52
线程窗口2销售电影票: 51
线程窗口1销售电影票: 51
线程窗口3销售电影票: 50
线程窗口2销售电影票: 49
线程窗口3销售电影票: 49
线程窗口1销售电影票: 48
线程窗口3销售电影票: 47
线程窗口2销售电影票: 47
线程窗口1销售电影票: 46
线程窗口3销售电影票: 45
线程窗口1销售电影票: 44
线程窗口2销售电影票: 45
线程窗口3销售电影票: 43
线程窗口2销售电影票: 43
线程窗口1销售电影票: 43
线程窗口3销售电影票: 42
线程窗口2销售电影票: 41
线程窗口1销售电影票: 40
线程窗口2销售电影票: 39
线程窗口3销售电影票: 38
线程窗口1销售电影票: 37
线程窗口1销售电影票: 36
线程窗口2销售电影票: 35
线程窗口3销售电影票: 36
线程窗口3销售电影票: 34
线程窗口2销售电影票: 33
线程窗口1销售电影票: 32
线程窗口2销售电影票: 31
线程窗口3销售电影票: 29
线程窗口1销售电影票: 30
线程窗口1销售电影票: 28
线程窗口3销售电影票: 28
线程窗口2销售电影票: 28
线程窗口3销售电影票: 27
线程窗口2销售电影票: 25
线程窗口1销售电影票: 26
线程窗口3销售电影票: 24
线程窗口1销售电影票: 22
线程窗口2销售电影票: 23
线程窗口2销售电影票: 21
线程窗口1销售电影票: 21
线程窗口3销售电影票: 21
线程窗口2销售电影票: 20
线程窗口1销售电影票: 19
线程窗口3销售电影票: 20
线程窗口2销售电影票: 18
线程窗口3销售电影票: 17
线程窗口1销售电影票: 18
线程窗口3销售电影票: 15
线程窗口2销售电影票: 16
线程窗口1销售电影票: 16
线程窗口3销售电影票: 14
线程窗口1销售电影票: 13
线程窗口2销售电影票: 12
线程窗口3销售电影票: 11
线程窗口1销售电影票: 11
线程窗口2销售电影票: 11
线程窗口3销售电影票: 10
线程窗口2销售电影票: 10
线程窗口1销售电影票: 10
线程窗口3销售电影票: 9
线程窗口2销售电影票: 8
线程窗口1销售电影票: 9
线程窗口1销售电影票: 7
线程窗口3销售电影票: 5
线程窗口2销售电影票: 6
线程窗口1销售电影票: 3
线程窗口2销售电影票: 3
线程窗口3销售电影票: 4
线程窗口3销售电影票: 2
线程窗口2销售电影票: 1
线程窗口1销售电影票: 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
程序出现了两个问题:
相同的票数,比如2这张票被卖了两回。
不存在的票,比如0票与-1票,是不存在的。
# 4.3. 问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
综上所述,线程安全问题根本原因
:
多个线程在操作共享的数据;
操作共享数据的线程代码有多条;
多个线程对共享数据有写操作;
# 4.4. 问题解决-线程同步
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。
同步代码块(synchronized)
同步方法(synchronized)
同步锁(ReenreantLock)
特殊域变量(volatile)
局部变量(ThreadLocal)
阻塞队列(LinkedBlockingQueue)
原子变量(Atomic*)
# 同步代码块(synchronized)
# 同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法:
synchronized(同步锁){
需要同步操作的代码
}
2
3
4
5
# 同步锁
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
锁对象可以是任意类型。
多个线程要使用同一把锁。
注意:
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块代码如下:
/**
* @Author: CHGGX
* @Description: <h1> 同步代码块解决售票问题 </h1>
*/
public class Ticket implements Runnable {
/**
* 电影票数据量,默认100张
*/
private int ticketNum = 100;
/**
* 创建锁对象
*/
private Object obj = new Object();
/**
* 多线程问题:
* 原因: 线程安全问题都是由全局变量及静态变量引起的
* 根本原因:
* - 多个线程在操作共享的数据;
* - 操作共享数据的线程代码有多条;
* - 多个线程对共享数据有写操作;
* <p>
* 解决:
* 1) `同步代码块(synchronized)`
* 2) `同步方法(synchronized)`
* 3) `同步锁(ReenreantLock)`
* 4) 特殊域变量(volatile)
* 5) 局部变量(ThreadLocal)
* 6) 阻塞队列(LinkedBlockingQueue)
* 7) 原子变量(Atomic*)
* <p>
* synchronized:
* 1. 创建锁对象
* 2. 同步代码块
* synchronized(){
*
* }
*/
public void run() {
while (true) {
synchronized (obj) {
// 判断是否有票
if (ticketNum > 0) {
try {
// 有票,让线程睡眠100ms
Thread.sleep(100);
// 打印当前售出的票数字和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "销售电影票: " + ticketNum--);
// 票数据-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
执行结果如下:线程的安全问题,解决了。
线程窗口3销售电影票: 100
线程窗口3销售电影票: 99
线程窗口3销售电影票: 98
线程窗口2销售电影票: 97
...
线程窗口2销售电影票: 4
线程窗口2销售电影票: 3
线程窗口2销售电影票: 2
线程窗口2销售电影票: 1
2
3
4
5
6
7
8
9
# 同步方法(synchronized)
# 同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
2
3
4
5
# 同步锁是谁?
``对于非static方法,同步锁就是this。`
对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
/**
* @Author: CHGGX
* @Description: <h1> 同步方法解决售票 </h1>
*/
public class Ticket implements Runnable {
/**
* 电影票数据量,默认100张
*/
private int ticketNum = 100;
/**
* 多线程问题:
* 原因: 线程安全问题都是由全局变量及静态变量引起的
* 根本原因:
* - 多个线程在操作共享的数据;
* - 操作共享数据的线程代码有多条;
* - 多个线程对共享数据有写操作;
* <p>
* 解决:
* 1) `同步代码块(synchronized)`
* 2) `同步方法(synchronized)`
* 3) `同步锁(ReenreantLock)`
* 4) 特殊域变量(volatile)
* 5) 局部变量(ThreadLocal)
* 6) 阻塞队列(LinkedBlockingQueue)
* 7) 原子变量(Atomic*)
*/
public void run() {
while (true) {
// 同步方法线程安全处理
saleTicket();
}
}
/**
* 同步方法
*/
private synchronized void saleTicket(){
// 判断是否有票
if (ticketNum > 0) {
try {
// 有票,让线程睡眠100ms
Thread.sleep(100);
// 打印当前售出的票数字和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "销售电影票: " + ticketNum--);
// 票数据-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
执行结果如下:线程的安全问题,解决了。
点击查看
线程窗口3销售电影票: 100
线程窗口3销售电影票: 99
线程窗口3销售电影票: 98
线程窗口2销售电影票: 97
...
线程窗口2销售电影票: 4
线程窗口2销售电影票: 3
线程窗口2销售电影票: 2
线程窗口2销售电影票: 1
2
3
4
5
6
7
8
9
# 同步锁(ReenreantLock)
# 同步锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
# 同步锁方法
public void lock() :加同步锁。
public void unlock() :释放同步锁。
2
3
使用重入锁代码如下:
/**
* @Author: CHGG
* @Description: <h1> 同步锁解决售票 </h1>
*/
public class Ticket implements Runnable {
/**
* 电影票数据量,默认100张
*/
private int ticketNum = 100;
/**
* 创建锁对象(重入锁)
* fair: 为true 公平锁,多个线程都公平拥有执行权
*/
private Lock lock = new ReentrantLock(true);
/**
* 多线程问题:
* 原因: 线程安全问题都是由全局变量及静态变量引起的
* 根本原因:
* - 多个线程在操作共享的数据;
* - 操作共享数据的线程代码有多条;
* - 多个线程对共享数据有写操作;
* <p>
* 解决:
* 1) `同步代码块(synchronized)`
* 2) `同步方法(synchronized)`
* 3) `同步锁(ReenreantLock)`
* 4) 特殊域变量(volatile)
* 5) 局部变量(ThreadLocal)
* 6) 阻塞队列(LinkedBlockingQueue)
* 7) 原子变量(Atomic*)
* 同步锁
* 1. 创建锁对象(重入锁)
* 2.
*/
public void run() {
while (true) {
// 加同步锁
lock.lock();
try {
// 判断是否有票
if (ticketNum > 0) {
try {
// 有票,让线程睡眠100ms
Thread.sleep(100);
// 打印当前售出的票数字和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "销售电影票: " + ticketNum--);
// 票数据-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
// 释放同步锁
lock.unlock();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
执行结果如下:线程的安全问题,解决了。
点击查看
线程窗口3销售电影票: 100
线程窗口3销售电影票: 99
线程窗口3销售电影票: 98
线程窗口2销售电影票: 97
...
线程窗口2销售电影票: 4
线程窗口2销售电影票: 3
线程窗口2销售电影票: 2
线程窗口2销售电影票: 1
2
3
4
5
6
7
8
9
# 4.5. 小结
# Synchronized和Lock区别?
synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
# 5. 线程死锁 😈
# 5.1. 什么是死锁
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力
。然而,并发执行也带来了新的问题--死锁
。
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
# 5.2. 死锁产生的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
- 互斥条件
- 不可剥夺条件
- 请求与保持条件
- 循环等待条件
# 互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
# 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
# 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
# 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。
循环等待
满足条件但无死循环
# 死锁示例代码
DeadLock.java
/**
* @Author: CHGGX
* @Description: <h1> 死锁 </h1>
*/
public class DeadLockRunnable implements Runnable {
/**
* 决定线程走向的标记
*/
private int flag;
/**
* 锁对象
* obj1: 锁对象1
* obj2: 锁对象2
* static: 所有的实例都共享
* 定义成静态变量,使线程可以共享实例
*
* 重点: 请求的资源为共享资源
*/
private static Object obj1 = new Object();
private static Object obj2 = new Object();
/**
* 构造函数
*/
public DeadLockRunnable(int flag) {
this.flag = flag;
}
public void run() {
if (flag == 1) {
// 线程1执行代码
// 同步代码块
synchronized (obj1){
System.out.println(Thread.currentThread().getName()+"以获取到资源obj1,正在请求资源obj2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+"已经获取到资源obj1和obj2");
}
}
} else {
// 线程2执行代码
// 同步代码块
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+"以获取到资源obj2,正在请求资源obj1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println(Thread.currentThread().getName()+"已经获取到资源obj1和obj2");
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
DeadLockTest.java
/**
* @Author: CHGGX
* @Description: <h1> 死锁测试 </h1>
*/
public class DeadLockTest {
public static void main(String[] args) {
// 1. 创建2个DeadLockRunnable实例,flag = 1 ,flag = 2
DeadLockRunnable runnable1 = new DeadLockRunnable(1);
DeadLockRunnable runnable2 = new DeadLockRunnable(2);
// 2. 创建两个线程执行2个DeadLockRunnable实例
Thread thread1 = new Thread(runnable1,"runnable1");
Thread thread2 = new Thread(runnable2,"runnable2");
// 3. 启动线程
thread1.start();
thread2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
执行效果如下:只打印两个flag值,表示死锁产生
runnable1以获取到资源obj1,正在请求资源obj2
runnable2以获取到资源obj2,正在请求资源obj1
2
# 5.3. 死锁处理
预防死锁
:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。避免死锁
:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。检测死锁
:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。解除死锁
:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
# 死锁预防
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。
破坏“互斥”条件
“互斥”条件是无法破坏的
。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
# 死锁避免
避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。
有序资源分配法
该算法实现步骤如下:
必须为所有资源统一编号,例如打印机为1、传真机为2、磁盘为3等
同类资源必须一次申请完,例如打印机和传真机一般为同一个机器,必须同时申请
不同类资源必须按顺序申请
例如:有两个进程P1和P2,有两个资源R1和R2
P1请求资源:R1、R2
P2请求资源:R1、R2
这样就破坏了环路条件,避免了死锁的发生。
银行家算法
银行家算法 (opens new window)(Banker's Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁 (opens new window)的算法。
设进程i提出请求REQUEST [i],则银行家算法按如下规则进行判断。
如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。
如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。
系统试探分配资源,修改相关数据:
AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数
ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数
NEED[i]-=REQUEST[i];//需要资源数-请求资源数
- 系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。
顺序加锁
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
例如以下两个线程就会死锁:
Thread 1:
lock A (when C locked)
lock B (when C locked)
wait for C
Thread 2:
wait for A
wait for B
lock C (when A locked)
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如以下两个线程就不会死锁
Thread 1:
lock A
lock B
lock C
Thread 2:
wait for A
wait for B
wait for C
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景。
限时加锁
限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:
Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁。
这种方式有两个缺点:
当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环。
Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。
# 死锁检测
预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复。
死锁检测数据结构:
E是现有资源向量(existing resource vector),代码每种已存在资源的总数
A是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
C是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数
R是请求矩阵(request matrix),R的每一行代表P所需要的资源的数量
死锁检测步骤:
寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
如果没有这样的进程,那么算法终止
算法结束时,所有没有标记过的进程都是死锁进程。
# 死锁恢复
利用抢占恢复。
临时将某个资源从它的当前所属进程转移到另一个进程。
这种做法很可能需要人工干预,主要做法是否可行需取决于资源本身的特性。
利用回滚恢复
周期性的将进程的状态进行备份,当发现进程死锁后,根据备份将该进程复位到一个更早的,还没有取得所需的资源的状态,接着就把这些资源分配给其他死锁进程。
通过杀死进程恢复
最直接简单的方式就是杀死一个或若干个进程。
尽可能保证杀死的进程可以从头再来而不带来副作用。
# 6. 线程通讯 😈
# 6.1. 为什么要线程通信
多个线程并发执行时,在默认情况下CPU是随机切换线程的
,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。
# 6.2. 线程通讯方式
线程间通信常用方式如下:
休眠唤醒方式:
Object的wait、notify、notifyAll
wait: 等待执行
notify: 通知其他线程执行
notifyAll: 通知所有等待的线程执行
Condition的await、signal、signalAll
CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行
CyclicBarrier:一组线程等待至某个状态之后再全部同时执行
Semaphore:用于控制对某组资源的访问权限
# 休眠唤醒方式
多线程打印奇偶数:
i从0开始,当时奇数时,技术线程大金,偶数线程等待;当时偶数时,技术线程大金,奇数线程等待.
2
Object的wait、notify、notifyAll
/**
* @Author: CHGGX
* @Description: <h1> </h1>
*/
public class OddEventTest {
/**
* 要打印的数
*/
private int i = 0;
/**
* 创建锁对象
*/
private Object obj = new Object();
/**
* 奇数打印方法,由奇数线程使用
*/
public void odd() {
// 1. 判i断是否小于10
while (i < 10) {
// 加锁
synchronized (obj) {
// 2. 小于10打印奇数 "%": 取余
if (i % 2 == 1) {
System.out.println("奇数: " + i);
i++;
// 唤醒偶数线程打印
obj.notify();
} else {
try {
// 不成立,打印偶数
// 此处为奇数线程,等待偶数线程打印完毕
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 偶数打印方法,由偶数线程使用
*/
public void event() {
// 1. 判i断是否小于10
while (i < 10) {
// 加锁
synchronized (obj) {
// 2. 小于10打印偶数 "%": 取余
if (i % 2 == 0) {
System.out.println("偶数: " + i);
i++;
// 唤醒奇数线程打印
obj.notify();
} else {
try {
// 不成立,打印奇数
// 此处为偶数线程,等待奇数线程打印完毕
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 测试
*
* @param args
*/
public static void main(String[] args) {
// 创建奇偶数对象
OddEventTest oddEventTest = new OddEventTest();
// 1. 开启奇数线程打印
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
oddEventTest.odd();
}
});
// 2. 开启偶数线程打印
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
oddEventTest.event();
}
});
// 3. 开启线程
thread1.start();
thread2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
调用Object的wait、notify、notifyAll等方法的调用,必须在
synchronized同步代码块或同步方法
内
Condition的await、signal、signalAll
/**
* @Author: CHGGX
* @Description: <h1> Condition等待唤醒的方法实现奇偶数打印 </h1>
*/
public class OddEventTest {
/**
* 要打印的数
*/
private int i = 0;
/**
* 创建锁对象
*/
private Lock lock = new ReentrantLock();
/**
* 创建Condition对象
*/
private Condition condition = lock.newCondition();
/**
* 奇数打印方法,由奇数线程使用
*/
public void odd() {
// 1. 判i断是否小于10
while (i < 10) {
// 加锁
lock.lock();
try {
// 2. 小于10打印奇数 "%": 取余
if (i % 2 == 1) {
System.out.println("奇数: " + i);
i++;
// 唤醒偶数线程打印
condition.signal();
} else {
try {
// 不成立,打印偶数
// 此处为奇数线程,等待偶数线程打印完毕
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
// 解锁
lock.unlock();
}
}
}
/**
* 偶数打印方法,由偶数线程使用
*/
public void event() {
// 1. 判i断是否小于10
while (i < 10) {
// 加锁
lock.lock();
try {
// 2. 小于10打印偶数 "%": 取余
if (i % 2 == 0) {
System.out.println("偶数: " + i);
i++;
// 唤醒奇数线程打印
condition.signal();
} else {
try {
// 不成立,打印奇数
// 此处为偶数线程,等待奇数线程打印完毕
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
// 解锁
lock.unlock();
}
}
}
/**
* 测试
*
* @param args
*/
public static void main(String[] args) {
// 创建奇偶数对象
OddEventTest oddEventTest = new OddEventTest();
// 1. 开启奇数线程打印
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
oddEventTest.odd();
}
});
// 2. 开启偶数线程打印
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
oddEventTest.event();
}
});
// 3. 开启线程
thread1.start();
thread2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
Condition在
Lock(互斥锁/共享锁)
的情况下执行
Object和Condition休眠唤醒区别
object wait()必须在synchronized(同步锁)下使用,
object wait()必须要通过Nodify()方法进行唤醒
condition await() 必须和Lock(互斥锁/共享锁)配合使用
condition await() 必须通过 signal() 方法进行唤醒
# CountDownLatch方式
CountDownLatch是在java1.5被引入的,存在于
java.util.concurrent
包下。CountDownLatch这个类能够使
一个线程等待其他线程完成各自的工作后再执行
。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
案例
教练训练运动员:
教练需要等待所有的运动员到齐且准备后之后,才开始训练.
2
示例代码:
/**
* @Author: CHGGX
* @Description: <h1> </h1>
*/
public class CoachRacerTest {
/**
* 设置要等待的运动员: 3个
*/
private CountDownLatch countDownLatch = new CountDownLatch(3);
/**
* 运动员方法,由运动员线程调用
*/
public void racer() {
// 1. 获取运动员线程名称
String name = Thread.currentThread().getName();
// 2. 运动员开始准备: 打印准备信息
System.out.println(name + "正在准备...");
// 3. 线程睡眠1s,来表示运动员在准备
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4. 运动员准备完毕: 打印准备完毕信息
System.out.println(name + "准备完毕!");
// 5. 计数-1
countDownLatch.countDown();
}
/**
* 教练方法,由教练线程调用
*/
public void coach() {
// 1. 获取教练线程称
String name = Thread.currentThread().getName();
// 2. 教练等待所有的运动员准备完毕: 打印等待信息
System.out.println(name + "等待所有运动员准备...");
// 3. 调用的countDownLatch的await方法等待其他线程执行完毕.
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4. 所有运动员已就绪,教练开始训练: 打印训练信息
System.out.println("所有运动员已就绪," + name + "开始训练!");
}
/**
* 测试
*
* @param args
*/
public static void main(String[] args) {
// 1. 创建CoachRacerTest示例
CoachRacerTest coachRacerTest = new CoachRacerTest();
// 2. 创建3个线程对象(运动员),调用CoachRacerTest的racer方法
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
coachRacerTest.racer();
}
},"运动员1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
coachRacerTest.racer();
}
},"运动员2");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
coachRacerTest.racer();
}
},"运动员3");
// 3. 创建1个线程对象(教练),调用CoachRacerTest的coach方法
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
coachRacerTest.coach();
}
},"教练");
// 4. 开启线程
thread4.start();
thread1.start();
thread2.start();
thread3.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
结果
教练等待所有运动员准备...
运动员3正在准备...
运动员2正在准备...
运动员1正在准备...
运动员3准备完毕!
运动员2准备完毕!
运动员1准备完毕!
所有运动员已就绪,教练开始训练!
2
3
4
5
6
7
8
# CyclicBarrier方式
CyclicBarrier是在java1.5被引入的,存在于
java.util.concurrent
包下。CyclicBarrier实现让
一组线程等待至某个状态之后再全部同时执行
。CyclicBarrier底层是
基于ReentrantLock(共享锁/重入锁)和Condition实现
。
三个线程同时启动,示例代码如下:
/**
* @Author: CHGGX
* @Description: <h1> 三个线程同时启动 </h1>
*/
public class ThreeThreadStartTest {
/**
* 参数: 参与CyclicBarrier的线程数
*/
private CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
public void startThread() {
// 1. 打印线程准备启动
String name = Thread.currentThread().getName();
System.out.println(name + "正在准备...");
// 2. 调用CyclicBarrier的await()方法,等待线程全部准备完成
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
// 3. 打印线程启动完毕信息
System.out.println(name + "已经启动完毕: " + System.currentTimeMillis());
}
public static void main(String[] args) {
// 1. 创建ThreeThreadStartTest示例
ThreeThreadStartTest threeThreadStartTest = new ThreeThreadStartTest();
// 2. 创建3个线程
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threeThreadStartTest.startThread();
}
},"线程1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threeThreadStartTest.startThread();
}
},"线程2");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
threeThreadStartTest.startThread();
}
},"线程3");
// 3. 启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
执行效果如下:三个线程同时启动
线程3正在准备...
线程1正在准备...
线程2正在准备...
线程2已经启动完毕: 1593417046711
线程3已经启动完毕: 1593417046711
线程1已经启动完毕: 1593417046711
2
3
4
5
6
# Semaphore方式
Semaphore是在java1.5被引入的,存在于
java.util.concurrent
包下。Semaphore用于
控制对某组资源的访问权限
。
工人使用机器工作
8个工人使用3台机器工作,机器为互斥资源(即每次只能一个人使用)
2
示例代码如下:
/**
* @Author: CHGGX
* @Date: 2020/06/29 15:55
* @Description: <h1> </h1>
*/
public class WorkMachineTest {
static class Work implements Runnable {
/**
* 工人的工号
*/
private int workNum;
/**
* 机器数
*/
private Semaphore semaphore;
public Work(int workNum, Semaphore semaphore) {
this.workNum = workNum;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 1. 工人获取机器
semaphore.acquire();
// 2. 打印工人获取到机器开始工作
String name = Thread.currentThread().getName();
System.out.println(name + "获取到机器,开始工作...");
// 3. 线程睡眠1s,模拟工人使用机器工作过程
Thread.sleep(1000);
// 4. 使用完毕,释放机器,打印工人使用完毕,释放机器
semaphore.release();
System.out.println(name + "使用完毕,释放机器");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 代表工人数8个
int workers = 8;
// 代表机器数3个
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < workers; i++) {
new Thread(new Work(i, semaphore), "工人" + i).start();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
执行效果如下:
点击查看
工人0获取到机器,开始工作...
工人1获取到机器,开始工作...
工人2获取到机器,开始工作...
工人3获取到机器,开始工作...
工人4获取到机器,开始工作...
工人1使用完毕,释放机器
工人2使用完毕,释放机器
工人0使用完毕,释放机器
工人5获取到机器,开始工作...
工人4使用完毕,释放机器
工人7获取到机器,开始工作...
工人6获取到机器,开始工作...
工人3使用完毕,释放机器
工人5使用完毕,释放机器
工人7使用完毕,释放机器
工人6使用完毕,释放机器
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 6.3. 小结
# sleep和wait区别
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中(有synchronized的同步代码块/同步方法)调用wait方法 ,方增获取抛出illegalMonitorStateException异常 | 不需要在同步方法/同步块中调用 |
作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在java.lang.Thread 中,作用于当前线程 |
释放锁资源 | 是 | 否 |
唤醒条件 | 其他线程调用对象的notify()获取notifyFall()方法 | 超时或者调动interrupt()方法体 |
方法属性 | wait是实例方法 | sleep是静态方法 |
# wait和notify区别
wait和notify都是Object中的方法
wait和notify执行前线程都必须获得对象锁
wait的作用是使当前线程进行等待
notify的作用是通知其他等待当前线程的对象锁的线程