前言

线程通讯的本质,其实就是通知和控制,而在一个线程中,通过十八般武艺去控制其他线程的方法,就是线程通讯实现方式。

其目的是为了线程之间更好的协作,从而完成一些复杂的工作。

线程通讯的几种方式

想要实现线程之间的通讯,方式方法非常的多,下面我们举一个很简单的线程题目,通过几种比较常见的方法去完成这道题目,从而理解线程之间通讯的过程和方法。

演示案例
1
2
假设有2个线程,一个线程仅打印数字,一个线程仅打印字母。而需求是要实现数字和字母交替打印的效果,
并且第一个打印的必须是数字,如:1A2B3C.. 应该如何去实现呢?
notify + wait方式

使用notify和wait的时候呢,我们必须先使用关键字synchronized加锁对象,否则是无法使用对象的这两个方法的。

所以准确的说,应该是synchronized + notify/wait的实现方式。

主要方法:

1
2
3
4
5
notify(): 随机唤醒一个等待的线程

notifyAll(): 唤醒所有等待的线程

wait(): 使当前线程进入等待状态

演示案例实现:

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

// 定义一个对象,用于synchronized锁,为什么要锁定一个公共对象而不直接用this?因为匿名类的
// 原因,用this其实是锁定的线程各自的匿名类
private static final Object obj = new Object();

private static final String[] LETTER = new String[]{"A","B","C","D"};
private static final Integer[] NUMBER = new Integer[]{1,2,3,4};

public static void main(String[] args) {
new Thread(() -> {
synchronized (obj){
try {
// 数字优先输出,所以字母先进入等待队列 等待叫醒
obj.wait();
for (String str : LETTER){
// 打印字母
System.out.print(str);
// 通知其他线程运行
obj.notify();
// 当前线程让出锁,进入等待队列
obj.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 保证线程正常关闭
obj.notify();
}
},"LETTER").start();

new Thread(() -> {
synchronized (obj){
for (Integer num : NUMBER){
// 打印数字
System.out.print(num);
// 通知其他线程运行
obj.notify();
try {
// 当前线程让出锁,进入等待队列
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 保证线程正常关闭
obj.notify();
}
},"NUMBER").start();
}
}
LockSupport方式

LockSupport是一个工具类,内部所有的方法都是静态的,而其功能,主要就是对线程进行阻塞和唤醒。

同样的,我们通过LockSupport控制线程的阻塞和唤醒,也是可以轻易完成以上案例要求的。

主要方法:

1
2
3
4
5
6
7
void part(): 阻塞当前线程

void parkUntil(long deadline): 阻塞当前线程,并指定截止时间(单位:13位的时间戳)

void parkNanos(long nanos): 阻塞当前线程,并设置超时时间(单位:纳秒,1秒=1000000000L纳秒)

unpark(Thread thread): 唤醒指定线程

演示案例实现:

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

// 定义2个线程,用于分别打印字母和数字
private static Thread thread4letter = null, thread4number = null;

private static final String[] LETTER = new String[]{"A","B","C","D"};
private static final Integer[] NUMBER = new Integer[]{1,2,3,4};

public static void main(String[] args) {
thread4letter = new Thread(() -> {
// 优先输出数字,字母线程阻塞
LockSupport.park();
for (String str : LETTER){
// 打印字母
System.out.print(str);
// 唤醒数字线程
LockSupport.unpark(thread4number);
// 阻塞当前线程,可以被unpark唤醒
LockSupport.park();
}
// 保证线程正常关闭
LockSupport.unpark(thread4number);
},"LETTER");

thread4number = new Thread(() -> {
for (Integer num : NUMBER){
// 打印数字
System.out.print(num);
// 唤醒字母线程
LockSupport.unpark(thread4letter);
// 阻塞当前线程,可以被unpark唤醒
LockSupport.park();
}
// 保证线程正常关闭
LockSupport.unpark(thread4letter);
},"NUMBER");

// 启动2个线程
thread4letter.start();
thread4number.start();
}
}
Lock + Condition方式

Lock + Condition来实现这个案例,相对来说是比较优雅的,因为它可以给锁指定多个条件,我们通过操控条件,就可以轻易的在不同场景下完成对锁的控制,从而完成对线程的控制。

演示案例实现:

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

// 定义一个可重入锁
private static final Lock lock = new ReentrantLock();

// 定义锁的字母线程条件
private static final Condition letterCondition = lock.newCondition();

// 定义锁的数字线程条件
private static final Condition numberCondition = lock.newCondition();

private static final String[] LETTER = new String[]{"A","B","C","D"};
private static final Integer[] NUMBER = new Integer[]{1,2,3,4};

public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
// 优先输出数字,字母线程等待
letterCondition.await();
for (String str : LETTER){
// 打印字母
System.out.print(str);
// 通知数字线程运行
numberCondition.signal();
// 本线程等待
letterCondition.await();
}
// 保证线程正常关闭
numberCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"LETTER").start();

new Thread(() -> {
lock.lock();
try {
for (Integer num : NUMBER){
// 打印数字
System.out.print(num);
// 通知字母线程运行
letterCondition.signal();
// 本线程等待
numberCondition.await();
}
// 保证线程正常关闭
letterCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"NUMBER").start();
}
}
volatile方式

volatile关键字保证了不同线程,对变量进行操作的可见性,以及读和写的原子性,而且它禁止指令重排,所以它还具备有序性。

因此,我们可以通过这个关键字特性,能够轻易的完成以上案例的要求。

演示案例实现:

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
public class VolatileTest {
// 定义flag,=1 运行数字线程,=2 运行字母线程,优先打印数字,初始设置为1,
private static volatile int flag = 1;

private static final String[] LETTER = new String[]{"A","B","C","D"};
private static final Integer[] NUMBER = new Integer[]{1,2,3,4};

public static void main(String[] args) {
new Thread(() -> {
for (String str : LETTER){
// 当flag=2的时候才打印字母
while (flag != 2){}
// 打印
System.out.print(str);
// 设置flag为1,数字
flag = 1;
}
},"LETTER").start();

new Thread(() -> {
for (Integer num : NUMBER){
// 当flag=1的时候才打印数字
while (flag != 1){}
// 打印
System.out.print(num);
// 设置flag为1,字母
flag = 2;
}
},"NUMBER").start();
}
}

AtomicInteger方式

我们都知道java并发机制中主要有三个特性需要我们去考虑:原子性、可见性和有序性。

synchronized关键字可以保证可见性和有序性却无法保证原子性,而AtomicInteger的作用就是为了保证原子性。

通过它的原子性,我们可以像volatile一样,轻易的完成案例想要的效果,

演示案例实现:

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

// 定义AtomicInteger flag,=1 运行数字线程,=2 运行字母线程,优先打印数字,初始设置为1,
private static AtomicInteger flag = new AtomicInteger(1);

private static final String[] LETTER = new String[]{"A","B","C","D"};
private static final Integer[] NUMBER = new Integer[]{1,2,3,4};

public static void main(String[] args) {
new Thread(() -> {
for (String str : LETTER){
// 当flag=2的时候才打印字母
while (flag.get() != 2){}
// 打印
System.out.print(str);
// 设置flag为1,数字
flag.set(1);
}
},"LETTER").start();

new Thread(() -> {
for (Integer num : NUMBER){
// 当flag=1的时候才打印数字
while (flag.get() != 1){}
// 打印
System.out.print(num);
// 设置flag为2,字母
flag.set(2);
}
},"NUMBER").start();
}
}