2.5 线程通信
不像进程间内存资源是独立的,各线程间共享地址和数据空间。因此,最简单的通信方式就是在一个线程中更新全局变量的值,在另一个线程中,通过无限循环去测试这个值,但这样无疑是很浪费处理器资源的。
2.5.1 传统线程通信
在一开始,java就支持了线程,线程间通过wait/notify/notifyAll通信,属于互斥量的形式。
/**
* 阻塞当前线程,直到另一个线程调用了该对象的notify或notifyAll方法
*/
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
-
调用wait方法后,当前线程释放所持有的该对象的锁,进入等待队列,等待其他线程调用对象的notify或notifyAll方法后,重新进入就绪队列。即暗指该线程首先得拥有该对象的锁。
-
wait/notify/notifyAll必须在同步方法/同步块中调用,使用while而不是if测试条件变量
// Thread-1 synchronized(mutex) { while(!condition){ try { mutex.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("thread-1 business"); } // Thread-2 synchronized(mutex) { if(!condition){ // do something condition = true; mutex.notifyAll(); } }
1).如果没有放在同步块中,mutex.wait()调用释放锁时没有持有锁,会抛出
java.lang.IllegalMonitorStateException
,如果在同步块中,当进入同步块中时,肯定持有了对象的锁。2).再者,如果没有同步,当线程1进入while循环块,调用wait之前,线程2执行了
condition = true; mutex.notifyAll();
,最后线程1调用wait,则线程1无法再有机会被唤醒,会一直在阻塞状态
/** 在等待队列中随机唤起一个线程 */
public final native void notify();
/** 唤起所有等待队列中的线程 */
public final native void notifyAll();
-
调用notify/notifyAll的线程必须持有锁
-
调用notify/notifyAll不会释放锁,如果在Thread-2中加入
while (true) ;
,则Thread-1无法被唤醒了synchronized (mutex) { if (!condition) { // do something condition = true; mutex.notifyAll(); while (true) ; } }
下面,看一道面试题:
/**
* 下面代码打印值多少?,取消注释后打印值多少?
*/
public class WaitThread {
public static void main(String[] args) {
CalcThread calcThread = new CalcThread();
calcThread.start();
synchronized (calcThread) {
// try {
// calcThread.wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("Total is: " + calcThread.total);
}
}
static class CalcThread extends Thread {
int total;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 100; i++) {
total += i;
}
notify();
}
}
}
}
2.5.2 基于Condition线程通信
在jdk1.5版中,引入了java.util.concurrent
工具包,将原来对象上的wait/notify/notifyAll同步方法功能从Object
独立出来,通过Condition(await/signal/SignalAll)提供另一种选择。
相较于wait/notify/notifyAll
+ synchronized
,Condition使用await/signal/SignalAll
+ Lock
实现相同甚至更灵活的功能。先看一个Condition源码注释文档里一个例子:
public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
//使用while,防止虚假唤醒(spurious wakeup)问题
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
- Condition关联一个Lock(基于CAS),而不是monitor实现,所以可以在一个同步实现中构造多个Condition(等待集)
- await()调用时释放lock并使用
LockSupport.park(this);
阻塞当前线程,等被唤醒后重新获取lock - signal()调用时先校验是否持有lock,然后从条件等待队列中唤醒一个线程(重新进入同步队列竞争锁),最后调用signal()的方法退出并释放锁(见take之
finally
)
2.5.3 基于阻塞队列(BlockingQueue)线程通信
基于线程安全的队列也可以实现线程间通信,常用的安全队列有:
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue
DelayQueue
阻塞队列BlockingQueue的实现和上面的BoundedBuffer差不多,同步队列SynchronousQueue采用自旋+CAS+LockSupport实现。
关于阻塞队列,以后再详细介绍
2.5.4 PipedInputStream/PipedOutputStream
线程间可以通过管道的方式通信,原理是通过一个缓冲buffer交换数据,通过同步方法connect建立管道连接。
public class PipedStreamTest {
private static PipedReader reader = new PipedReader();
private static PipedWriter writer = new PipedWriter();
static {
try {
reader.connect(writer);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(() -> {
IntStream.range(0, 10).forEach(i -> {
try {
System.out.println("read from pipe: " + reader.read());
} catch (IOException e) {
e.printStackTrace();
}
});
}, "reader").start();
new Thread(() -> {
IntStream.range(0, 10).forEach(i -> {
try {
writer.write(i);
System.out.println("write to pipe: " + i);
} catch (IOException e) {
e.printStackTrace();
}
});
}, "writer").start();
}
}
- 不要在同一个线程中同时使用PipedInputStream和PipedOutputStream,会造成死锁
- 不要同时从输入流connect到输出流,又反向连接
- 缓冲区为空或满时,会阻塞当前线程
