Java多线程
Easul Lv6

相关链接

相关理解

  • 进程:分配应用程序的内存空间
  • 线程:负责进程中内容执行的控制单元,也叫执行路径或执行情景
  • 多线程:一个进程可以有多个执行路径。一个进程至少有一个线程
  • 任务:每个线程要运行的内容
  • CPU切换线程执行可以时间片来切换执行不同进程。随机切换。

好处

可以让多部分同时运行

弊端

不同的进程开启了很多的线程,CPU分给每个线程的执行时间变少,因此线程运行变慢,效率变低(可以加CPU)

JVM的多线程

  • JVM启动时启动了多个线程,至少可以分析出两个线程
    • 主线程(main函数执行),任务代码都定义在main函数
    • 垃圾回收线程(GC),任务代码定义在垃圾回收器内部
    • 其他
  • 主线程结束,其他线程不一定结束。所有线程结束,jvm结束。
  • finalize():子类重写该方法可以执行其他清除,不写的话,直接默认被垃圾回收器清除。Object类的方法
  • System.gc():运行垃圾回收器,告诉垃圾回收器收垃圾,什么时候回收垃圾是随机的。

多线程创建

  • windows当中,任务管理器创建需要的进程和线程
  • 主线程名是main
  • 继承Thread类来创建线程
    • 定义一个类继承Thread类
    • 覆盖Thread类中的run()方法,run()调用需要执行的代码
    • 建一个该类的对象,创建线程
    • 调用start()方法启动线程,run()方法自动运行
    • 一个类有父类,就不要继承Thread了(不然多继承了)
    • 快速创建线程可以使用匿名内部类
  • 也可以实现Runnable接口

Thread常用方法

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getName(): 获取线程的名称,线程对象创建后,名称就定义了。运行run()方法也可以获取其名称
currentThread(): 返回当前正在执行线程的引用
start(): 开启一个线程
stop(): 停止当前所有线程的运行(危险,慎用)
sleep(): 使当前线程睡眠
wait(): 使当前线程处于冻结状态
notify(): 从线程池中唤醒一个线程
interrupt(): 中断当前的wait或sleep等的状态
setDaemon(true): 设置线程为守护线程,需要在start()前设置。
- 前台线程全部结束,该线程会自动结束,无论是否是冻结状态。
- 守护线程也叫后台线程,true标记为守护线程
join(): 执行该方法就是冻结当前运行线程,执行该线程。
- 该线程运行完,再继续运行刚刚中止的线程。可能抛出异常InterruptedException
toString(): 返回字符串形式的线程名称,包括优先级和线程组
- 优先级: 获取CPU执行权的几率,越大获取几率越高(1-10)
- 线程组: 线程的组的划分,可以创建线程的时候指定
setPriority(Thread.MAX_PRIORITY): 设置进程执行优先级,
- MAX_PRIORITY 10
- MIN_PRIORITY 1
- NORM_PRIORITY 5(默认为5)
yield(): 释放当前线程执行权。

继承Thread的多线程

JAVA
1
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
// 三个线程,自定义的两个,主线程一个
class Test{
public static void main(String[] args){
/*
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码同时运行
运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中
自定义的线程的任务在哪里?
Thread类用于描述线程,线程是需要任务的,所以Thread类也对任务描述
这个任务通过Thread类的run()方法来体现(run方法封装了自定义线程的任务)
run()方法中定义了线程运行的任务代码
*/
DemoThread d1 = new DemoThread("旺财");
DemoThread d2 = new DemoThread("qiang");
// 开启线程,调用run()方法
// 会发现main,旺财,qiang三个线程会轮流执行
d1.start();
for(int x = 0; x < 20; x++){
System.out.println(x+">>>"+Thread.currentThread().getName());
}
d2.start();
}
}
class DemoThread extends Thread{
private String name;

DemoThread(String name){
// 调用父类构造函数,自动修改进程名称
super(name);
}

// 重写run()就是为了运行自定义任务
@Override
public void run(){
show();
}

public void show(){
for(int x = 0; x < 10; x++){
for(int y = -999999999; y < 999999999; y++){}
// 获取当前运行线程的名字: Thread.currentThread().getName()
System.out.println(name + "....x=" + x + "....name=" + Thread.currentThread().getName());
}
}
}

上述代码运行在栈中的体现

  • 每一个线程一个栈
    • 主函数先运行main(),开启一个栈
    • 到了d1.start(),运行run(),开启一个栈
    • 到了d2.start(),运行run(),开启一个栈
  • 每个线程中的代码在各自的栈中分别执行
  • 如果主线程或其他某个线程出现异常,该线程执行中断,不影响其他线程。
    image

实现Runnable的多线程

用于有父类,但需要创建多线程,这个更常用

  • 定义类实现Runnable接口,覆盖run()方法,将线程任务代码封装到run()中
  • 创建Thread对象,传入实现了Runnable接口的子类对象,即可调用线程该对象创建的任务
  • 向Thread对象传入Runnable接口子类对象是因为它封装了任务代码,传进去,线程开启前才能运行指定任务。

Runnable多线程示例

JAVA
1
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
class Test{
public static void main(String[] args){
// 创建了一个任务
DemoThread d = new DemoThread();
// 传入实现了Runnable接口的对象,相当于分配了任务
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
// 执行任务
t1.start();
t2.start();
for(int i = 0; i < 10; i++){
for(int j = -999999999; j < 999999999; j++){}
System.out.println(i+"..."+Thread.currentThread().getName());
}
}
}
class DemoThread implements Runnable{
public void show(){
for(int i = 0; i < 10; i++){
for(int j = -999999999; j < 999999999; j++){}
System.out.println(i+"..."+Thread.currentThread().getName());
}
}

@Override
public void run(){
show();
}
}

传入Runnable对象的解释

Thread接收实现了Runnable的对象,并对其执行

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Thread {
private Runnable r;

Thread(){

}

Thread(Runnable r){
// 传入的Runnable对象相当于在这里将引用赋值给了内部
this.r = r;
}

public void run(){
// 运行的时候看传入自定义的任务没,传入就执行自定义的
if(r != null)
r.run();
}

public void start(){
run();
}
}

Runnable接口的好处

  • 将线程任务从线程子类分离出来,将线程的任务进行对象封装,不必继承Thread所有方法。
  • 避免了Java单继承局限性
  • 与Thread相比有了思想上的变化,继承Thread是让任务成为他的一部分,实现Runnable是将任务封装成一个对象。
  • Thread也实现了Runnable是因为和其他对象可以向上抽取任务的run()方法。

线程的状态

YAML
1
2
3
4
5
6
7
8
start(): 线程从创建到运行
run(): 线程从运行到消亡(运行完了线程就消亡了)
stop(): 线程从运行到消亡(主动关闭线程,不安全)
sleep(time): 线程从运行到冻结,time(毫秒)时间后运行或临时阻塞
wait(): 线程从运行到冻结(一直冻结)
notify(): 线程从冻结到运行或临时阻塞
CPU执行资格: 可以被CPU处理,在处理队列中排队
CPU执行权: 正在被CPU处理

image

线程使用

同一段代码,好多人都需要同时用,可以封装成线程。

Runtime异常的情况

  • 功能没问题,传值出现问题
  • 功能状态发生问题
    • Thread t = new Thread(); t1.start();
    • 线程已开启,处于一种状态,再开启会出现问题

线程安全

可以参看实际问题中的买票代码

范例解释

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 因为在线程执行的时候有随机性,假如说现在num=1,有t1和t2两个线程执行如下方法
// CPU执行t1,先判断num为1大于0,然后转换执行t2,此时num不变还是1大于0
// 切换到t1,执行输出,1,切换到t2输出0,因而输出了不应该输出的数据。有安全隐患
// 可以加上sleep()方法观察一下
public void sale(){
while(true){
if(num > 0){
/*sleep可能抛出异常,但是实现的接口没有抛出异常,所以不能声明只能catch*/
/*try{
Thread.sleep(10);这里会释放执行权
}
catch(InterruptedException e){

}*/
System.out.println(Thread.currentThread().getName()+">>>>"+num--);
}
else
break;
}
}

线程安全产生的原因

  • 多个线程操作共享数据
  • 操作共享数据的代码超过一条

线程安全问题的解决

使用同步

  • 将操作共享数据的多条代码封装成整体(使用同步代码块),有一个线程在执行这个代码,其他线程不能执行,该线程执行完,其他线程继续执行。
  • 表现形式
    • 同步代码块
    • 同步函数(synchronized可以修饰函数)
  • 好处
    • 解决了线程安全问题。
  • 弊端
    • 相对降低了效率(执行同步代码的线程不会一直占有CPU,执行权转到外部线程时,外部线程会一直判断同步锁,却无法执行代码)
  • 前提
    • 必须有多个线程使用同一个锁(锁写到方法里,每个线程一个锁,相当于没加,所以写到成员变量里)

同步代码块格式

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 代码解释:
* 一个线程先判断该对象是否被持有,没有被持有则占用他,执行里边的代码,执行到sleep()的时候相当于释放了执行权
* CPU转而执行其他线程,但是每个线程在执行同步代码的时候,先判断对象持有情况,一直都被持有,则无法执行同步代码
* 直到第一个线程sleep()结束并执行完接下来的同步代码,才会释放对象的持有。这个时候CPU再次挑选线程执行相应代码。
*/

// 放一个标记对象,相当于整个任务对象的标记(锁,同步锁)
// 后边用于同步代码监视,不能放到方法里,要放到成员变量里
synchronized(对象){
// 放需要被同步代码
if(num > 0){
try{
Thread.sleep(10);
}
catch(InterruptedException e){

}
System.out.println(Thread.currentThread().getName()+">>>>"+num--);
} else {
break;
}
}

同步函数格式

  • 将函数同步,一个线程占有该函数之后,除非将里边的所有代码全部执行完,否则不会放开该函数占有权。就算放开执行权,其他线程无法占有该函数,也无法执行,只能最开始的线程执行。
  • 所以只将需要同步的代码封装到函数里即可,然后调用该函数
  • 同步函数用的锁是this对象(synchronized(this))。
JAVA
1
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
// 同步函数
public synchronized void run(){
while(true){
if(num > 0){
try{
Thread.sleep(10);
} catch(InterruptedException e) {

}
System.out.println(Thread.currentThread().getName()+">>>"+num--);
} else {
break;
}
}
}

// 其他方法调用同步函数
// 一个线程占了该函数,其他线程就占不了了
public void run(){
while(true){
show();
}
}
public synchronized void show(){
if(num > 0){
try{
Thread.sleep(10);
}
catch(InterruptedException e){

}
System.out.println(Thread.currentThread().getName()+">>>"+num--);
}
}

同步函数与同步代码块区别

  • 同步函数的锁只能是this对象synchronized(this),同步代码块可以用不同的对象。
  • 同步代码块用了this,可以简写为同步函数。
  • 建议使用同步代码块

测试同步函数和同步代码块是否在用同一个锁

JAVA
1
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
// 当运行run()方法的时候,通过flag来判断当前线程要运行哪一个run()方法,t1默认运行同步代码块
// 为了不让t1.start()和t2.start()运行的太快,中间加一个睡眠
// 来达到先让t1的线程判断flag,然后再执行main线程后边的方法。
// t1线程运行起来之后,flag被修改为false,然后运行同步函数。
// 同步函数默认调用当前对象锁,同步代码块传入的是this对象,
// 所以两个线程操作的是同一个锁,不会再有线程安全问题。
class Ticket implements Runnable {
private int num = 100;
boolean flag = true;

@Override
public void run() {
if (flag) {
while(true) {
synchronized(this) {
if(num > 0) {
try {
Thread.sleep(10);
} catch(InterruptedException e) {

}
System.out.println(Thread.currentThread().getName()+">>>code>>>"+num--);
}
}
}
} else {
while(true){
show();
}
}
}

public synchronized void show(){
if(num > 0){
try{
Thread.sleep(10);
}
catch(InterruptedException e){

}
System.out.println(Thread.currentThread().getName()+">>>function>>>"+num--);
}
}
}
class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch(InterruptedException e) {
}
t.flag = false;
t2.start();
}
}

静态同步函数

使用的锁是当前字节码文件所属的对象
this.getClass()可以从对象获取当前静态函数所在字节码文件所属的对象
类名.class可以直接获取当前静态函数所在字节码文件所属的对象(用静态属性获取)

JAVA
1
2
3
4
5
6
7
8
9
10
11
public static synchronized void show(){
if(num > 0){
try{
Thread.sleep(10);
}
catch(InterruptedException e){

}
System.out.println(Thread.currentThread().getName()+">>>function>>>"+num--);
}
}

单例模式的懒汉式和饿汉式的线程安全问题

JAVA
1
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
// 饿汉式
class Single{
private static final Single s = new Single();
private Single(){
}
public static Single getInstance(){
// 将这个方法放到多线程返回没有问题,因为只有一句话且变量已经是final
return s;
}
}
// 懒汉式-同步函数
class Single{
private static Single s = null;
private Single(){

}
public static synchronized Single getInstance(){
// 将该方法放到多线程中返回可能会有安全隐患,同时调用可能返回不同的对象
// 有多句话,执行顺序可能不同
// 加了同步函数就没问题你了,但降低了效率
if(s == null)
Single = new Single();
return s;
}
}
// 懒汉式-同步代码块
class Single {
private static Single s = null;
private Single() {

}
public static Single getInstance() {
// 不能用this.getClass(),因为这个用于非静态对象获取字节码文件对象
// 前边s为空的时候会判断锁,不为空则不判断锁,减少了锁资消耗
// 加锁解决线程安全问题,多加一个if提高了效率,解决了安全和效率。
// 虽然多加了if,但判断s的时候这个不可少
if(s == null){
synchronized(Single.class){
if(s == null)
Single = new Single();
}
}
return s;
}
}

同步中的死锁

常见情景

同步嵌套:一共两个锁,两个线程,每个线程必须同时拿到两个锁才能进行执行功能,但是每个线程只拿到一个,想用对方的,但对方都没放开,产生死锁。
下边的代码在调用的过程中会在执行到某次的时候,每个线程各自拿到一个锁(this或obj),想得到对方那个,却得不到

JAVA
1
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
class Lock implements Runnable{
private boolean flag;
Lock(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
synchronized(MyLock.locka){
System.out.println(Thread.currentThread().getName()+"if>>>locka");
synchronized(MyLock.lockb){
System.out.println(Thread.currentThread().getName()+"if>>>lockb");
}
}
}
else{
synchronized(MyLock.lockb){
System.out.println(Thread.currentThread().getName()+"else>>>lockb");
synchronized(MyLock.locka){
System.out.println(Thread.currentThread().getName()+"else>>>locka");
}
}
}
}
}
class MyLock{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class Test{
public static void main(String[] args){
Lock la = new Lock(true);
Lock lb = new Lock(false);
Thread t1 = new Thread(la);
Thread t2 = new Thread(lb);
t1.start();
t2.start();
}
}

线程通信

背景

多个线程处理同一资源,但是任务不同(四个车同时往进拉煤,三个车同时往外运煤)

  • 赋值:input
  • 取值:output
  • 资源:resource

线程间等待唤醒机制

下边三个方法(监视器方法)都必须定义在同步中,用于操作线程状态,必须要明确操作的是哪个锁的线程。

  • wait():让线程处于冻结状态,会抛出中断异常(InterruptedException)
    • 释放CPU执行权和执行资格,并将该线程存到该锁的线程池
  • notify():唤醒该锁线程池中的一个任意线程(使线程具备执行资格)
  • notifyAll():唤醒该锁线程池中所有线程(使线程具备执行资格)

sleep与wait的异同

  • sleep必须指定时间,wait可以不指定时间
  • sleep和wait都释放了CPU执行权
  • sleep没有释放线程锁,wait释放了线程锁

  • 操作的是哪个锁,就用哪个锁的方法来处理该线程
  • 锁也叫监视器。
  • 线程操作方法放到了Object类中,是因为这些方法都是监视器(锁)的方法,任意对象都可以作为监视器(锁),故而放到里边

等待唤醒示例

示例是线程间等待唤醒机制,适用于单生产消费模式

JAVA
1
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/*
* Input和Output都需要使用同一个对象,但使用单例全局就只能有一个对象
* 所以可以创建好对象,然后将对象传进来。
*
* wait()
* notify()
* notifyAll()
*/
class Resource{
String name;
String sex;
// 用来标记资源是否被赋值。
boolean flag = false;
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run(){
int x = 0;
while(true){
synchronized(r){
if(r.flag) {
try{
r.wait();
}
catch(InterruptedException e){

}
}
if(x == 0){
r.name = "Easul";
r.sex = "male";
} else {
r.name = "王文博";
r.sex = "女";
}
r.flag = true;
r.notify();
}
x =(++x) % 2;
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run(){
while(true){
synchronized(r){
if(!r.flag) {
try{
r.wait();
}
catch(InterruptedException e){

}
}
System.out.println(r.name+">>>"+r.sex);
r.flag = false;
r.notify();
}
}
}
}
class Test{
public static void main(String[] args){
/*
创建资源
创建任务
创建线程
开启线程
*/
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
// ==========================================================
/*
将成员变量私有化之后,变量的赋值在成员方法里执行,所以应该在方法里加锁
同样把等待和唤醒拿过来就可以用了
对于变量的赋值,因为是使用this锁,所以使用同步函数
*/
class Resource{
private String name;
private String sex;
// 用来标记资源是否被赋值。
private boolean flag = false;
public synchronized void set(String name, String sex) {
if(flag) {
try{
// wait()醒了之后,往下走,不再判断flag
this.wait();
}
catch(InterruptedException e){

}
}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out(){
if(!flag) {
try{
this.wait();
}
catch(InterruptedException e){

}
}
System.out.println(this.name+"<<<"+this.sex);
flag = false;
this.notify();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run(){
int x = 0;
while(true){
if(x == 0){
r.set("Easul", "male");
}
else{
r.set("王文博", "女");
}
x =(++x) % 2;
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run(){
while(true){
r.out();
}
}
}
class Test{
public static void main(String[] args){
/*
创建资源
创建任务
创建线程
开启线程
*/
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}

多生产者和多消费者

多次生产和死锁原因

  • if判断标记,只判断一次,可能导致不该运行的线程运行,数据出现错误。
  • notify只能唤醒一个线程,有很大几率无法唤醒对方线程。(加while会导致死锁)

解决多次生产和死锁

  • while判断标记,解决了线程获取执行权后是否执行的问题。
  • notifyAll解决了本方线程一定能够唤醒对方线程的问题。(唤醒了所有线程,降低了效率,需要判断本方线程)

示例代码

JAVA
1
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
/*
多生产者:t0 t1
多消费者:t2 t3
假设t0先拿到CPU,生产烤鸭1,通知线程池其他线程运行(随机运行一个),线程池没有,则从阻塞队列中挑一个。自己wait()
t1拿到CPU,t1判断有烤鸭,wait()
t2拿到CPU,t2判断有烤鸭,消费烤鸭1,通知线程池其他某个线程运行(t0或t1),t0唤醒,自己wait()
t3拿到CPU,t3判断没有烤鸭,wait()
t0拿到CPU,生产烤鸭2,通知线程池其他线程运行(t1 t2或t3),t1唤醒,自己wait()
t1拿到CPU,生产烤鸭3,通知线程池其他线程运行(t0 t2或t3),自己wait()
于是有了烤鸭2和烤鸭3,烤鸭2无法消费
这里问题在于t1生产烤鸭3的时候不知道已经生产了烤鸭2,所以可以在判断一下有没有生产过(while)。
直接while,按上述步骤,最后唤醒t1,然后全部wait()
但是因为无法唤醒某个指定线程所以会死锁.只能全部唤醒。
*/
class Resource{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name){
// 判断自己等待后是否已经有线程做过自己做过的事情
while(flag) {
try{
wait();
}
catch(InterruptedException e){
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"生产了"+this.name);
this.flag = true;
// 避免没人工作而唤醒所有线程
notifyAll();
}
public synchronized void out(){
while(!flag) {
try{
wait();
}
catch(InterruptedException e){
}
}
System.out.println(Thread.currentThread().getName()+"消费了"+this.name);
this.flag = false;
notifyAll();
}
}
class Producer implements Runnable{
private Resource r;
Producer(Resource r){
this.r = r;
}
public void run(){
while(true)
r.set("烤鸭");
}
}
class Consumer implements Runnable{
private Resource r;
Consumer(Resource r){
this.r = r;
}
public void run(){
while(true)
r.out();
}
}
class Test{
public static void main(String[] args){
Resource r = new Resource();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
Thread t0 = new Thread(p);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
t0.start();
t1.start();
t2.start();
t3.start();
}
}

锁的升级

使用java.util.concurrent.locks包中的Lock接口Condition接口

  • Lock接口替代了synchronized,将同步和锁封装成了对象
  • Condition接口替代了Object监视器方法(wait notify notifyAll),将其方法封装成了对象。
  • 一个Lock可以挂多个Condition。每个Condition都有一组监视器

Lock与synchronized的区别

  • synchronized对于锁的操作是隐式的,看不到获取与释放锁的过程
  • Lock对于锁的操作是可控的

Lock相关方法

YAML
1
2
3
lock(): 开启锁
unlock(): 释放锁
newCondition(): 获取一组监视器对象,类型为Condition,同一个锁可以获取多组监视器

Condition相关方法

YAML
1
2
3
await(): 将线程从运行到冻结
signal(): 将线程从冻结到阻塞
signalAll(): 将线程池中所有冻结线程恢复到阻塞

多生产者多消费者升级版

JAVA
1
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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
class Resource{
private String name;
private int count = 1;
private boolean flag = false;
// 创建了锁对象,ReentrantLock是互斥锁
private Lock lock = new ReentrantLock();
// 通过一个锁,获取多个监视器,监视不同线程
private Condition producer_con = lock.newCondition();
private Condition consumer_con = lock.newCondition();
public void set(String name){
// 上锁
lock.lock();
try{
while(flag) {
// 等待时放到了生产者线程池,用时唤醒生产者线程池即可
// 等待时会释放锁
try{
producer_con.await();
}
catch(InterruptedException e){
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"生产了"+this.name);
this.flag = true;
// 唤醒消费者监视器,只唤醒一个就可以了,提高了效率
consumer_con.signal();
} finally {
// 释放锁,如果发生异常可以放到finally
lock.unlock();
}
}
public void out(){
lock.lock();
try{
while(!flag) {
try{
consumer_con.await();
}
catch(InterruptedException e){
}
}
System.out.println(Thread.currentThread().getName()+"消费了"+this.name);
this.flag = false;
producer_con.signal();
} finally {
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resource r;
Producer(Resource r){
this.r = r;
}
public void run(){
while(true)
r.set("烤鸭");
}
}
class Consumer implements Runnable{
private Resource r;
Consumer(Resource r){
this.r = r;
}
public void run(){
while(true)
r.out();
}
}
class Test{
public static void main(String[] args){
Resource r = new Resource();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
Thread t0 = new Thread(p);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
t0.start();
t1.start();
t2.start();
t3.start();
}
}

注意:同步里只能有一个线程执行,但是活着的可以不止一个

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
如果t0 t1 t2全部调用show(),进入wait()状态,t3调用method(),然后notifyAll()
那么show()中的t0 t1 t2都会唤醒,可能会获得执行权,
但是只要没有拿到同步锁,就无法向下执行代码,
所以可以获取执行权,但是不会向下执行代码。谁有锁谁执行
*/
class Demo{
void show() {
wait();
}
void method() {
wait();
code
notifyAll();
}
}

停止线程几种方式

  • 中断:一种冻结状态,中止正在运行的状态
  • 线程的stop()方法
  • 线程的run()方法结束
    • 任务中都有循环结构,控制循环就可以结束任务
    • 控制循环结束用标记来控制,常用。while(flag){}
    • 如果线程处于wait(冻结状态)或sleep,就无法结束线程(无法读取到标记)
      • 使用interrupt(),中断当前wait()或sleep()的状态使其具备CPU执行资格
      • 因为是强制执行,wait或sleep可能会抛出InterruptedException

买票

JAVA
1
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
/*
卖票100张
4个窗口(同时卖)
*/
/*
1 用继承,需要创建四个线程,每个线程都执行100次,不符合实际
2 创建一个线程,运行四次。错误方式,一个线程只能开启一次
3 使用静态的num,但如果有多种100张票,num无法实现多种使用
4 使用实现,将任务封装,然后交给线程处理。
*/
class Ticket implements Runnable{
// 100张票
private int num;
Ticket(int num){
this.num = num;
}
// 买票的代码需要被同时使用
public void sale(){
while(true){
if(num > 0)
System.out.println(Thread.currentThread().getName()+">>>>"+num--);
else
break;
}
}
public void run(){
sale();
}
}
class Test{
public static void main(String[] args){
// 创建线程任务对象
Ticket t = new Ticket(100);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}

代码中问题如下

  • 对于这个代码来说,创建了任务对象,放到堆中,创建了四个线程,每个线程所使用的数据都是这个任务对象的数据,从而实现了数据共享,不需要静态。
  • 运行后出现的输出不规律
    • 多核CPU交替运行可能导致运算的顺序不同
    • 输出和运算顺序不一样(参考异常输出和其他线程共同运行的情况)
  • 会有线程安全,可能会买同一张票,或者有负数的票

输出1还是2

JAVA
1
2
3
4
5
6
7
8
9
10
11
// 传入Thread的对象实现了run方法,但是后边相当于子类对象又重写了该方法
// 所以运行更新的方法输出2
new Thread(new Runnable(){
public void run(){
System.out.println(1);
}
}){
public void run(){
System.out.println(2);
}
}.start();
 评论