kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

进程间通信.md

发表于 2020-08-10 更新于 2021-11-21 分类于 并发
本文字数: 11k 阅读时长 ≈ 10 分钟

线程之间通讯

每个线程都是独立运行的个体,线程通讯能让多个线程之间协同工作。

  • Object类中的wait/notify方法可以实现线程间通讯
  • wait/notify必须与synchronized一起使用,因为使用这些方法之前必须获得monitor锁
  • wait释放锁,且只释放当前对象锁,若有多个对象,不会影响到其他对象的锁
  • notify不释放锁

wait方法被唤醒的四种情况

  • 另一个线程调用这个wait方法加锁对象的notify方法且刚好被唤醒的是本线程
  • 另一个线程调用这个wait方法加锁对象的notifyAll方法
  • 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
  • 线程自身调用了interrupt()

栗子1:while方式实现线程间通讯

启动一个线程调用put方法往全局变量list中添加元素,当添加了5个元素后,通知另一个线程调用get方法获取list中元素并输出

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
public class ThreadDemo17 {
private volatile List<String> list = new ArrayList<String>();
private volatile boolean canGet = false;

public void put() {
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add("A");
System.out.println("线程" + Thread.currentThread().getName() + "添加第" + i + "个元素");
if (i == 5) {
//循环到第次则通知其他线程开始获取数据进行处理
canGet = true;
System.out.println("线程" + Thread.currentThread().getName() + "发出通知");
}
}
}

public void get() {
while (true) {
if (canGet) {
for (String s : list) {
System.out.println("线程" + Thread.currentThread().getName() + "获取元素:" + s);
}
break;
}
}
}

public static void main(String[] args) {
ThreadDemo17 demo = new ThreadDemo17();
// 先添加元素
new Thread(() -> demo.put(), "t1").start();
// 后获取元素
new Thread(() -> demo.get(), "t2").start();

}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
线程t1添加第1个元素
线程t1添加第2个元素
线程t1添加第3个元素
线程t1添加第4个元素
线程t1添加第5个元素
线程t1发出通知
线程t2获取元素:A
线程t2获取元素:A
线程t2获取元素:A
线程t2获取元素:A
线程t2获取元素:A
线程t1添加第6个元素
线程t1添加第7个元素
线程t1添加第8个元素
线程t1添加第9个元素
线程t1添加第10个元素

疑问:

1.一个线程修改全局变量,另外一个线程获取变量,不是会产生脏读吗?为什么没有加synchronized保证同步执行?

​ 因为有全局变量canGet的控制,只有通过线程1改变了canGet的值,线程2才能get,并不会引起脏读。

这里需要对canGet变量加上volatile关键字,保证可见性。在个数为5的时候,线程t1已经更新了canGet=true,但是还未更新到主内存中,则线程t2取到的值不是最新的值。

2.当线程1,线程2运行结束后,list的size是多少?

​ 10,当线程1添加到5个元素后,通知线程2,线程2获取元素并输出,然后就break退出循环了。但是线程1还在继续添加元素,直到线程1的循环10都结束。所以,最后list的size肯定是10,只不过线程2只是输出了前5个元素,就退出循环,不再输出了。

栗子2:wait/notify实现线程间通讯

启动一个线程调用put方法往全局变量list中添加元素,当添加了5个元素后,通知另一个线程调用get方法获取list中元素并输出。先调用get方法,先保证get方法运行。

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
public class ThreadDemo18 {
//原子类
private volatile List<String> list = new ArrayList<String>();
private Object lock = new Object();

public void put() {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add("A");
System.out.println("线程" + Thread.currentThread().getName() + "添加第" + i + "个元素");
if (list.size() == 5) {
//数据准备好了,发出唤醒通知,但是不释放锁
lock.notify();
System.out.println("发出通知...");
}
}
}
}

public void get() {
synchronized (lock) {
try {
System.out.println("线程" + Thread.currentThread().getName() + "业务处理,发现有需要的数据没准备好,则发起等待");
System.out.println("线程" + Thread.currentThread().getName() + "wait");
lock.wait(); //wait操作释放锁,否则其他线程只能等该方法执行完后才能进入put方法
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
for (String s : list) {
System.out.println("线程" + Thread.currentThread().getName() + "获取元素:" + s);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
ThreadDemo18 demo = new ThreadDemo18();
// 先调用get方法
new Thread(() -> demo.get(), "t1").start();

// 保证get方法先被调用
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 再调用put方法
new Thread(() -> demo.put(), "t2").start();

}
}

运行结果:

线程t1先启动,获取lock锁,进入get方法执行。执行到lock.wait时,释放锁,等待通知。

t1释放锁,线程t2获得锁,进入put方法执行,添加5个元素后,发出通知,但是notify不释放锁,会继续执行完t1后才释放锁。

当t1添加完10个元素,t1执行完成,释放锁。线程t2接着执行wait后的输出语句,输出list中的10个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
线程t1业务处理,发现有需要的数据没准备好,则发起等待
线程t1wait
线程t2添加第1个元素
线程t2添加第2个元素
线程t2添加第3个元素
线程t2添加第4个元素
线程t2添加第5个元素
发出通知...
线程t2添加第6个元素
线程t2添加第7个元素
线程t2添加第8个元素
线程t2添加第9个元素
线程t2添加第10个元素
线程t1被唤醒
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A
线程t1获取元素:A

notify唤醒

  • notify方法只应该被拥有该对象的monitor的线程调用
  • 要等刚才执行notify的线程退出被synchronized保护的代码并释放monitor

notify与notifyAll的区别

notifyAll 使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

notify 只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

栗子1:只调用一次notify

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
public class ThreadDemo19 {
public synchronized void run1() {
System.out.println("进入run1方法..");
this.notify();
System.out.println("run1执行完毕,通知完毕..");
}

public synchronized void run2() {
try {
System.out.println("进入run2方法..");
this.wait();
System.out.println("run2执行完毕..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized void run3() {
try {
System.out.println("进入run3方法..");
this.wait();
System.out.println("run3执行完毕..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
ThreadDemo19 demo = new ThreadDemo19();
// 先执行等待通知的线程
new Thread(() -> demo.run2()).start();
new Thread(() -> demo.run3()).start();
Thread.sleep(1000L);
// 再执行发送通知的线程
new Thread(() -> demo.run1()).start();
}
}

执行结果:

run2,run3方法先运行,等待通知。sleep1s后,run1方法运行,使用notify发送通知,只会发送一个通知。

run2收到通知,执行完毕。run3没有收到通知,还在等待着notify/notifyAll才可以继续运行。

1
2
3
4
5
进入run2方法..
进入run3方法..
进入run1方法..
run1执行完毕,通知完毕..
run2执行完毕..

如果run2方法修改成如下:

1
2
3
4
5
6
7
8
9
10
11
public synchronized void run2() {
try {
System.out.println("进入run2方法..");
this.wait();
System.out.println("run2执行完毕..");
this.notify();
System.out.println("run2发出通知..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

执行结果:

在run2方法中再次发送了notify通知,此时只有run3一个方法在等待通知,自然就是run3接收到通知,执行完毕。可以看到,notify类似于链式操作。

1
2
3
4
5
6
7
进入run2方法..
进入run3方法..
进入run1方法..
run1执行完毕,通知完毕..
run2执行完毕..
run2发出通知..
run3执行完毕..

栗子2:使用notifyAll

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
public class ThreadDemo19 {
public synchronized void run1() {
System.out.println("进入run1方法..");
this.notifyAll();
System.out.println("run1执行完毕,通知完毕..");
}

public synchronized void run2() {
try {
System.out.println("进入run2方法..");
this.wait();
System.out.println("run2执行完毕..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized void run3() {
try {
System.out.println("进入run3方法..");
this.wait();
System.out.println("run3执行完毕..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
ThreadDemo19 demo = new ThreadDemo19();
// 先执行等待通知的线程
new Thread(() -> demo.run2()).start();
new Thread(() -> demo.run3()).start();
Thread.sleep(1000L);
// 再执行发送通知的线程
new Thread(() -> demo.run1()).start();
}
}

执行结果:

使用notifyAll通知,所有的等待通知的线程都会收到通知。

1
2
3
4
5
6
进入run2方法..
进入run3方法..
进入run1方法..
run1执行完毕,通知完毕..
run3执行完毕..
run2执行完毕..

守护进程与用户进程

  • main函数所在线程是一个用户线程
  • 只要有一个用户线程还没结束,jvm进程就不会结束
  • 父线程结束后,子线程还可以继续存活,子线程的生命周期不受父线程影响

线程上下文切换

当前线程使用完时间片后就会进入就绪状态,让出cpu执行权给其他线程,此时就是从当前线程的上下文切换到了其他线程。

当发生上下文切换的时候需要保存执行现场,待下次执行时进行恢复。

所以频繁的,大量的上下文切换会造成一定资源开销。

sleep方法

  • Thread类中的一个静态方法
  • 暂时让出执行权,不参与CPU调度,但是不释放锁,包括synchronized和lock
  • sleep之后,本身进入阻塞状态,时间到了就进入就绪状态,一旦获取到CPU时间片,则继续执行
  • 清除中断状态

一句话总结:Sleep方法可以让线程进入TIME_WAITING状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常且清除中断标志

栗子1:两个线程交替打印1-100,使用wait/notify

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
// 两个线程执行自己的for循环,用判断是否奇偶的方式来控制打印顺序
public class OddExample {

public static void main(String[] args) {
Object lock = new Object();

Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":偶数------:" + i);
try {
// 输出了偶数之后等待唤醒
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
lock.notify();
}
}
}

});


Thread thread2 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":奇数:" + i);
try {
// 输出奇数后等待被唤醒
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
lock.notify();
}
}
}

});

thread1.start();
thread2.start();
}
}
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
// 共享变量i,控制好i初始值,直接打印,打印完唤醒其他线程打印,不需要判断是否奇偶
public class OddExample4 {

public static void main(String[] args) {
PrintThread printThread = new PrintThread();
new Thread(printThread).start();
new Thread(printThread).start();
}
}

class PrintThread implements Runnable {

private int i = 0;
private Object lock = new Object();

@Override
public void run() {
// 直接打印,打印完换醒其他线程,自己休眠,不需要再判断是否奇偶
while (i <= 100) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
lock.notify();
// 一定要判断是小于100的才阻塞等待,否则当i>100的时候,没有线程notify了,会导致线程一直被阻塞
if (i <= 100) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

栗子2:两个线程交替打印1-100,使用volatile修饰的变量控制

用volatile修饰的变量来交替执行线程,需要while(true)死循环检测变量是否改变.
当输出完结果后,线程不会正常结束,会被阻塞

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
public class OddExample2 {

public volatile boolean is_odd = true;
private volatile int i = 0;

public void odd() {
while (true) {
if (is_odd && i < 100) {
System.out.println(Thread.currentThread().getName() + ":偶数:" + i);
i++;
is_odd = false;
}
}
}

public void ji() {
while (true) {
if (!is_odd && i < 100) {
System.out.println(Thread.currentThread().getName() + ":奇数:" + i);
i++;
is_odd = true;
}
}
}

public static void main(String[] args) {
OddExample2 obj = new OddExample2();
new Thread(() -> obj.odd()).start();
new Thread(() -> obj.ji()).start();
}
}

栗子3:两个线程交替打印1-100,使用synchronized

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
public class OddExample3 {

private static int i = 0;
private static Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
while (i < 100) {
// 代码块执行完释放锁后,可能又会进入该线程,两个线程会频繁竞争锁
synchronized (lock) {
if ((i & 1) == 0) { //位运算判断是否是偶数
System.out.println(Thread.currentThread().getName() + ":偶数:" + i);
i++;
}
}
}
}).start();

new Thread(() -> {
while (i < 100) {
synchronized (lock) {
if ((i & 1) == 1) { //位运算判断是否是偶数
System.out.println(Thread.currentThread().getName() + ":奇数:" + i);
i++;
}
}
}
}).start();
}
}

面试题1:为什么wait方法需要在同步代码块内使用,而sleep方法不需要

sleep方法主要是在线程内使用的,不涉及多个线程的通信。

wait方法放在同步代码块主要了为了防止死锁的发生。因为有了synchronized关键字,能保证在同一时刻,只会有一个线程执行代码快中的代码,避免了先notify,后wait这种情况。

面试题2:wait/notify方法为什么定义在Object类,而不是Thread类?

wait/notify是需要在同步代码块内使用的,而同步代码快涉及到锁,锁是保存在对象的对象头的。任意对象都可以作为锁。另一方面,经常在同一个代码块中有多个锁配合使用,因此wait/notify定义在Object类是比较灵活的。

面试题3:调用Thread.wait()方法会发生什么情况?

Thread也是一个对象,也可以调用wait方法。但是Thread类在线程结束后,会自动调用notifyAll方法。如果使用Thread对象作为锁,调用了Thread.wait方法,会对设计好的业务有影响。

面试题4:wait、sleep方法的相同点,不同点

  • 相同点
    • 阻塞
    • 响应中断
  • 不同点
    • 所属类
    • 释放锁:wait释放锁,sleep不释放锁
    • 同步方法中:wait需要在同步方法中使用,sleep不需要
    • 指定时间:sleep方法使用必须指定时间,而wait可以不指定时间
一毛也是爱~
Kim.Zhang 微信支付

微信支付

# 并发基础
Java内存模型.md
Unsafe.md
  • 文章目录
  • 站点概览
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
  1. 1. 线程之间通讯
  2. 2. wait方法被唤醒的四种情况
    1. 2.1. 栗子1:while方式实现线程间通讯
    2. 2.2. 栗子2:wait/notify实现线程间通讯
  3. 3. notify唤醒
  4. 4. notify与notifyAll的区别
    1. 4.1. 栗子1:只调用一次notify
    2. 4.2. 栗子2:使用notifyAll
  5. 5. 守护进程与用户进程
  6. 6. 线程上下文切换
  7. 7. sleep方法
  8. 8. 栗子1:两个线程交替打印1-100,使用wait/notify
  9. 9. 栗子2:两个线程交替打印1-100,使用volatile修饰的变量控制
  10. 10. 栗子3:两个线程交替打印1-100,使用synchronized
  11. 11. 面试题1:为什么wait方法需要在同步代码块内使用,而sleep方法不需要
  12. 12. 面试题2:wait/notify方法为什么定义在Object类,而不是Thread类?
  13. 13. 面试题3:调用Thread.wait()方法会发生什么情况?
  14. 14. 面试题4:wait、sleep方法的相同点,不同点
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%