JAVA线程间通信的几种方式

本文通过一个面试题探讨了Java中线程间的通信方法,提供了包括synchronized、Lock、CyclicBarrier在内的7种不同实现方案。

今天在群里面看到一个很有意思的面试题:
“编写两个线程,一个线程打印1~25,另一个线程打印字母A~Z,打印顺序为12A34B56C……5152Z,要求使用线程间的通信。”
这是一道非常好的面试题,非常能彰显被面者关于多线程的功力,一下子就勾起了我的兴趣。这里抛砖引玉,给出7种想到的解法。

1. 第一种解法,包含多种小的不同实现方式,但一个共同点就是靠一个共享变量来做控制

a. 利用最基本的synchronizednotifywait

     
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
     
public class MethodOne {
private final ThreadToGo threadToGo = new ThreadToGo();
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
try {
for ( int i = 0; i < arr.length; i=i+ 2) {
synchronized (threadToGo) {
while (threadToGo.value == 2)
threadToGo.wait();
Helper.print(arr[i], arr[i + 1]);
threadToGo.value = 2;
threadToGo.notify();
}
}
} catch (InterruptedException e) {
System.out.println( "Oops...");
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
try {
for ( int i = 0; i < arr.length; i++) {
synchronized (threadToGo) {
while (threadToGo.value == 1)
threadToGo.wait();
Helper.print(arr[i]);
threadToGo.value = 1;
threadToGo.notify();
}
}
} catch (InterruptedException e) {
System.out.println( "Oops...");
}
}
};
}
class ThreadToGo {
int value = 1;
}
public static void main(String args[]) throws InterruptedException {
MethodOne one = new MethodOne();
Helper.instance.run(one.newThreadOne());
Helper.instance.run(one.newThreadTwo());
Helper.instance.shutdown();
}
}

b. 利用LockCondition

     
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
     
public class MethodTwo {
private Lock lock = new ReentrantLock( true);
private Condition condition = lock.newCondition();
private final ThreadToGo threadToGo = new ThreadToGo();
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
try {
lock.lock();
while(threadToGo.value == 2)
condition.await();
Helper.print(arr[i], arr[i + 1]);
threadToGo.value = 2;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
try {
lock.lock();
while(threadToGo.value == 1)
condition.await();
Helper.print(arr[i]);
threadToGo.value = 1;
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
};
}
class ThreadToGo {
int value = 1;
}
public static void main(String args[]) throws InterruptedException {
MethodTwo two = new MethodTwo();
Helper.instance.run(two.newThreadOne());
Helper.instance.run(two.newThreadTwo());
Helper.instance.shutdown();
}
}

c. 利用volatile

volatile修饰的变量值直接存在main memory里面,子线程对该变量的读写直接写入main memory,而不是像其它变量一样在local thread里面产生一份copy。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
33
34
35
36
37
38
39
40
41
42
43
44
     
public class MethodThree {
private volatile ThreadToGo threadToGo = new ThreadToGo();
class ThreadToGo {
int value = 1;
}
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
while(threadToGo.value== 2){}
Helper.print(arr[i], arr[i + 1]);
threadToGo.value= 2;
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
while(threadToGo.value== 1){}
Helper.print(arr[i]);
threadToGo.value= 1;
}
}
};
}
public static void main(String args[]) throws InterruptedException {
MethodThree three = new MethodThree();
Helper.instance.run(three.newThreadOne());
Helper.instance.run(three.newThreadTwo());
Helper.instance.shutdown();
}
}

d. 利用AtomicInteger

     
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
     
public class MethodFive {
private AtomicInteger threadToGo = new AtomicInteger( 1);
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
while(threadToGo.get()== 2){}
Helper.print(arr[i], arr[i + 1]);
threadToGo.set( 2);
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
while(threadToGo.get()== 1){}
Helper.print(arr[i]);
threadToGo.set( 1);
}
}
};
}
public static void main(String args[]) throws InterruptedException {
MethodFive five = new MethodFive();
Helper.instance.run(five.newThreadOne());
Helper.instance.run(five.newThreadTwo());
Helper.instance.shutdown();
}
}

2. 第二种解法,是利用CyclicBarrierAPI;

CyclicBarrier可以实现让一组线程在全部到达Barrier时(执行await()),再一起同时执行,并且所有线程释放后,还能复用它,即为Cyclic。
CyclicBarrier类提供两个构造器:

     
1
2
3
4
5
     
public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

这里是利用它到达Barrier后去执行barrierAction。

     
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
     
public class MethodFour{
private final CyclicBarrier barrier;
private final List<String> list;
public MethodFour() {
list = Collections.synchronizedList( new ArrayList<String>());
barrier = new CyclicBarrier( 2,newBarrierAction());
}
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0, j= 0; i < arr.length; i=i+ 2,j++) {
try {
list.add(arr[i]);
list.add(arr[i+ 1]);
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
try {
list.add(arr[i]);
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
};
}
private Runnable newBarrierAction(){
return new Runnable() {
@Override
public void run() {
Collections.sort(list);
list.forEach(c->System.out.print(c));
list.clear();
}
};
}
public static void main(String args[]){
MethodFour four = new MethodFour();
Helper.instance.run(four.newThreadOne());
Helper.instance.run(four.newThreadTwo());
Helper.instance.shutdown();
}
}

这里多说一点,这个API其实还是利用lockcondition,无非是多个线程去争抢CyclicBarrier的instance的lock罢了,最终barrierAction执行时,是在抢到CyclicBarrierinstance的那个线程上执行的。

3. 第三种解法,是利用PipedInputStreamAPI;

这里用流在两个线程间通信,但是Java中的Stream是单向的,所以在两个线程中分别建了一个input和output。这显然是一种很搓的方式,不过也算是一种通信方式吧……-_-T,执行的时候那种速度简直。。。请不要BS我。

     
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
     
public class MethodSix {
private final PipedInputStream inputStream1;
private final PipedOutputStream outputStream1;
private final PipedInputStream inputStream2;
private final PipedOutputStream outputStream2;
private final byte[] MSG;
public MethodSix() {
inputStream1 = new PipedInputStream();
outputStream1 = new PipedOutputStream();
inputStream2 = new PipedInputStream();
outputStream2 = new PipedOutputStream();
MSG = "Go".getBytes();
try {
inputStream1.connect(outputStream2);
inputStream2.connect(outputStream1);
} catch (IOException e) {
e.printStackTrace();
}
}
public void shutdown() throws IOException {
inputStream1.close();
inputStream2.close();
outputStream1.close();
outputStream2.close();
}
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
private PipedInputStream in = inputStream1;
private PipedOutputStream out = outputStream1;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
Helper.print(arr[i], arr[i + 1]);
try {
out.write(MSG);
byte[] inArr = new byte[ 2];
in.read(inArr);
while( true){
if( "Go".equals( new String(inArr)))
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
private PipedInputStream in = inputStream2;
private PipedOutputStream out = outputStream2;
public void run() {
for ( int i = 0; i < arr.length; i++) {
try {
byte[] inArr = new byte[ 2];
in.read(inArr);
while( true){
if( "Go".equals( new String(inArr)))
break;
}
Helper.print(arr[i]);
out.write(MSG);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
}
public static void main(String args[]) throws IOException {
MethodSix six = new MethodSix();
Helper.instance.run(six.newThreadOne());
Helper.instance.run(six.newThreadTwo());
Helper.instance.shutdown();
six.shutdown();
}

4. 第四种解法,是利用BlockingQueue

顺便总结下BlockingQueue的一些内容。
BlockingQueue定义的常用方法如下:

  • add(Object):把Object加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛出异常。
  • offer(Object):表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
  • put(Object):把Object加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里有空间再继续。
  • poll(time):获取并删除BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。当不传入time值时,立刻返回。
  • peek():立刻获取BlockingQueue里排在首位的对象,但不从队列里删除,如果队列为空,则返回null。
  • take():获取并删除BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。

BlockingQueue有四个具体的实现类:

  • ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
  • LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。
  • PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
  • SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

这里我用了两种玩法:

  • 一种是共享一个queue,根据peekpoll的不同来实现;
  • 第二种是两个queue,利用take()会自动阻塞来实现。
     
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
     
public class MethodSeven {
private final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
Helper.print(arr[i], arr[i + 1]);
queue.offer( "TwoToGo");
while(! "OneToGo".equals(queue.peek())){}
queue.poll();
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
while(! "TwoToGo".equals(queue.peek())){}
queue.poll();
Helper.print(arr[i]);
queue.offer( "OneToGo");
}
}
};
}
private final LinkedBlockingQueue<String> queue1 = new LinkedBlockingQueue<>();
private final LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>();
public Runnable newThreadThree() {
final String[] inputArr = Helper.buildNoArr( 52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i=i+ 2) {
Helper.print(arr[i], arr[i + 1]);
try {
queue2.put( "TwoToGo");
queue1.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
public Runnable newThreadFour() {
final String[] inputArr = Helper.buildCharArr( 26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for ( int i = 0; i < arr.length; i++) {
try {
queue2.take();
Helper.print(arr[i]);
queue1.put( "OneToGo");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
public static void main(String args[]) throws InterruptedException {
MethodSeven seven = new MethodSeven();
Helper.instance.run(seven.newThreadOne());
Helper.instance.run(seven.newThreadTwo());
Thread.sleep( 2000);
System.out.println( "");
Helper.instance.run(seven.newThreadThree());
Helper.instance.run(seven.newThreadFour());
Helper.instance.shutdown();
}

本文所有代码已上传至GitHub:https://github.com/EdisonXu/POC/tree/master/concurrent-test

 本文由  EdisonXu - 徐焱飞 创作,采用  CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本位链接为 http://edisonxu.org/2017/03/02/java-thread-communication.html
<think>我们参考了用户提供的引用内容,其中提到了几种线程间通信的机制。根据这些引用和Java并发编程的知识,我们可以将Java线程间通信进行分类。 引用[1]提到wait()和notify(),引用[2]展示了使用wait()和notify()的例子,引用[5]详细说明了等待/通知机制。 引用[4]提到了Android中的Handler机制,但这是Android特有的,属于线程间通信的一种方式。 另外,我们还可以补充一些其他常见的线程间通信方式。 因此,我们可以将Java线程间通信(包括Android)分为以下几类: 1. **共享内存**:通过共享对象(变量)进行通信,需要配合锁(synchronized或Lock)来保证原子性和可见性。 - 包括使用volatile关键字修饰的变量,保证可见性。 - 也包括使用synchronized关键字保护的共享变量。 2. **等待/通知机制(wait/notify)**:这是Object类提供的方法,必须配合synchronized使用。 - 引用[2]和[5]中展示的就是这种方式。 3. **管道流(Piped Streams)**:包括PipedInputStream和PipedOutputStream,或者PipedReader和PipedWriter,用于在两个线程之间传递数据。 4. **使用并发工具类**:Java 5之后引入的java.util.concurrent包中的工具类,例如: - BlockingQueue(阻塞队列):如ArrayBlockingQueue、LinkedBlockingQueue等,用于生产者-消费者模型。 - CountDownLatch、CyclicBarrier、Semaphore等同步器。 - Exchanger:用于两个线程之间交换数据。 5. **Android平台特有的Handler机制**:引用[4]中提到的,通过Handler、Looper和MessageQueue实现线程间通信,主要用于Android的UI线程与其他线程通信。 6. **使用Future和Callable**:通过FutureTask和ExecutorService,可以在一个线程中获取另一个线程执行的结果。 7. **信号量(Semaphore)**:虽然属于并发工具类,但也可以单独作为一种通信方式,通过控制多个线程对共享资源的访问来实现通信。 8. **Lock和Condition**:与synchronized的wait/notify类似,但提供了更灵活的控制,如多个条件队列(Condition)。 注意:在分类时,我们尽量将各种机制按照其设计思想和使用方式归类。但有些机制可能同时涉及多种分类,比如BlockingQueue内部也是使用锁和条件变量(如ReentrantLock和Condition)实现的。 下面我们按照上述分类进行详细说明。</think>### Java 线程间通信的分类详解 根据线程通信的实现机制和场景,Java 线程间通信可分为以下五类: --- #### 一、共享内存通信(隐式通信) **原理**:通过共享变量实现通信,需要同步机制保证可见性 - **同步机制**: - `synchronized` 关键字(内置锁)[^2] - `volatile` 变量(保证可见性) - `Atomic` 原子类(CAS操作) ```java // 示例:共享变量通信 class SharedCounter { private int count = 0; // 共享变量 public synchronized void increment() { count++; // 同步方法保证原子性 } } ``` **特点**:效率高但需谨慎处理竞态条件[^1] --- #### 二、等待/通知机制(显式通信) **核心方法**:`wait()`, `notify()`, `notifyAll()`(Object类方法) ```java // 示例:生产者-消费者模型 synchronized (lock) { while (!condition) { lock.wait(); // 释放锁并等待 } // 执行操作... lock.notifyAll(); // 唤醒等待线程[^5] } ``` **实现要求**: 1. 必须在同步代码块内调用 2. 需循环检查条件避免虚假唤醒 3. `notify()`唤醒单个线程,`notifyAll()`唤醒所有[^2] --- #### 三、管道流通信(Piped Streams) **适用场景**:线程间传输字节流/字符流 ```java // 示例:管道字节流 PipedInputStream pin = new PipedInputStream(); PipedOutputStream pout = new PipedOutputStream(pin); // 线程1:写入数据 pout.write(data); // 线程2:读取数据 pin.read(); ``` **类型**: - `PipedInputStream`/`PipedOutputStream`(字节流) - `PipedReader`/`PipedWriter`(字符流) --- #### 四、高级并发工具通信 **Java 5+ 引入的并发包工具**: 1. **阻塞队列**(BlockingQueue)[^1] ```java BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); // 生产者 queue.put("data"); // 消费者 String data = queue.take(); ``` 2. **同步器**: - `CountDownLatch`(倒计时门闩) - `CyclicBarrier`(循环栅栏) - `Semaphore`(信号量) - `Exchanger`(数据交换点) 3. **Future/Callable**: ```java ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> "result"); String result = future.get(); // 阻塞获取结果 ``` --- #### 五、Android 特有通信机制 **Handler/Looper 机制**: ```java // 工作线程发送消息 Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { // 主线程处理消息 } }; // 子线程发送消息 new Thread(() -> { Message msg = handler.obtainMessage(); handler.sendMessage(msg); }).start(); ``` **组件**: - `Handler`:消息处理器 - `MessageQueue`:消息队列 - `Looper`:消息循环器[^4] --- ### 通信机制对比 | 类型 | 典型实现 | 适用场景 | 线程耦合度 | |-------------------|------------------------|----------------------------------|------------| | 共享内存 | synchronized/volatile | 简单状态共享 | 高 | | 等待/通知 | wait()/notify() | 生产者-消费者 | 中 | | 管道流 | PipedInputStream | 流式数据传输 | 低 | | 高级并发工具 | BlockingQueue | 复杂线程协作 | 低 | | Android 机制 | Handler | Android UI 线程与非UI线程通信 | 低 | --- ### 选择建议 1. **优先使用并发工具包**:`BlockingQueue`、`CountDownLatch`等简化开发[^1] 2. **简单同步选synchronized**:快速实现基础同步 3. **复杂协调用Lock/Condition**:需要多条件等待时 4. **Android开发用Handler**:保证UI线程安全[^4] 5. **避免管道流**:性能较差且易导致死锁 > **关键原则**:尽可能降低线程间耦合度,优先选择基于消息的通信模式[^1][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值