在Java中各个线程是并行运行的,实际开发中经常需要在线程中实现通信,最经典的场景就是生产者-消费者机制;或者保证部分线程的执行顺序,比如让线程A等待直到线程B执行完成之后。本篇文章将通过一些示例逐步说明实现线程通信的几种方式(感慨一下,为了深入浅出写demo很耗精力,但是收获还是挺多,纸上得来终觉浅,绝知此事要躬行!)。
一、通过共享变量实现线程通信
之前在Java内存模型中,分析过volatile关键字的实现原理和使用方法,使用volatile修饰的共享变量,当一个线程对变量执行了写操作之后,其他线程会失效对共享变量的拷贝,从主存中获取最新的值。这个过程其实就实现线程间的通信。
如下栗子模拟了8只乌龟赛跑,其中有乌龟率先到达终点,通知其他乌龟比赛结束!
public class ThreadCommunication {
private static final int MAX_LOOPS = 100;
private static final int NUM_THREADS = 8;
//使用volatile修饰共享变量,当某个线程修改了这个值后通知其他线程,从而实现线程间通信!
private static volatile boolean gameover = false;
private static volatile String winner;
private static final Object lock = new Object();
public static void main (String[] args) throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i=0; i
二、使用同步方式实现线程间通信
这个比较好理解,线程进入临界区需要获取锁,根据Java内存模型规定,同步块退出时的写对于后续同步块入口的读可见,所以可以通过在同步块中修改公共属性的值实现线程间的通信。
public class ThreadCommunication2 {
private static int count;
public static void main (String[] args) throws InterruptedException {
Thread t1 = new Thread(new Syncs(), "Thread1");
Thread t2 = new Thread(new Syncs(), "Thread2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("In main thread, count = " + count);
}
static final class Syncs implements Runnable {
@Override
public void run() {
synchronized (ThreadCommunication2.class) {
count++;
System.out.println("in thread " + Thread.currentThread().getName() + ", count = " + count);
}
}
}
}
运行结果:
in thread Thread1, count = 1
in thread Thread2, count = 2
In main thread, count = 2
三、基于wait和notify、notifyall方式实现生产者-消费者模式
需要注意的是,wait和notify都依赖于锁。
具体流程:线程A执行wait方法之前必须获得锁,当执行wait方法之后线程A主动释放锁,线程B获取锁之后,执行notify/notifyAll方法时,会将线程A从锁的等待队列中移到同步队列中,当线程B退出同步块释放锁的时候,线程A重新竞争锁。
notify和notifyAll的区别就是notify是把一个线程从等待队列移入同步队列,而notifyAll是锁对象上等待队列所有线程都移入同步队列。
图片说明:实线箭头是线程A的流程,虚线箭头是线程B的流程。
如下展示了一个简单的栗子,模拟一个工厂生产和销售面包过程
public class BreadFactory {
/**
* 使用无界队列保存产品作为仓库
*/
private static final ConcurrentLinkedQueue breads = new ConcurrentLinkedQueue();
/**
* 仓库保存产品的最大个数,当仓库中的产品个数超过这个时,生产者等待.
*/
private static final int MAX_BREADS_NUM = 100;
/**
* 仓库保存产品的最小个数,当仓库中产品个数比这个少时,消费者等待.
*/
private static final int MIX_BREAD_NUM = 50;
/**
* 线程通过wait和notify/notifyAll通信时必须配合锁
*/
private static final Object lock = new Object();
public static void main(String[] args) {
Thread producer = new Thread(new Producer(), "Producer");
Thread consumer = new Thread(new Consumer(), "Consumer");
//启动生产者和消费者线程
producer.start();
consumer.start();
}
//生产者线程
protected static final class Producer implements Runnable{
@Override
public void run() {
synchronized (lock) {
while (true) {
while (breads.size() == MAX_BREADS_NUM) {
try {
//生产者进行等待.
System.out.println("Producer is waiting..");
lock.wait();
//生产者被唤醒,重新竞争锁
System.out.println("Producer is wakeup..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bread bread = new Bread();
breads.add(bread);
System.out.println(String.format("Produce bread " + bread.getId()));
//生产者通知消费者可以执行消费了。
lock.notifyAll();
}
}
}
}
//消费者线程
protected static final class Consumer implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (true) {
while (breads.size() <= MIX_BREAD_NUM) {
try {
//消费者等待
System.out.println("Consumer is waiting..");
lock.wait();
//此时消费者被唤醒,重新竞争锁
System.out.println("Consumer is wakeup..");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bread bread = breads.poll();
System.out.println(String.format("Consume bread " + bread.getId()));
//消费产品后通知生产者
lock.notifyAll();
}
}
}
}
//产品实体类
protected static final class Bread {
private long id;
public Bread() {
super();
//此处简单地随机生成一个id
id = new Random().nextLong();
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
}
四、管道输入/输出流
管道输入/输出和普通的文件输入/输出流或者网络输入/输出流不同之处在于它主要用于线程间的数据传输,而传输的媒介是内存。管道输入/输出流主要包含4个具体实现:PipedOutputStream、PipedInputStream,PipedRead、PipedWriter,前两者面向字节,后两者面向字符。
核心思想就是一个线程往流里写数据,另外一个线程从流里读数据。
如下栗子是我偷懒从网上复制并本地执行的,因为很简单所以我没有再写了。
有个地方需要注意,需要调用connect将输入流和输出流进行连接!
public class PipeTest1 {
public static void main(String[] args) {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
try {
//将输入和输出流连接,重要!!
pis.connect(pos);
} catch (IOException e) {
e.printStackTrace();
}
new MyProducer(pos).start();
new MyConsumer(pis).start();
}
}
class MyProducer extends Thread {
private PipedOutputStream outputStream;
private int index = 0;
public MyProducer(PipedOutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void run() {
while (true) {
try {
for (int i = 0; i < 5; i++) {
outputStream.write(index++);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者每0.5秒从管道中取1件产品,并打印剩余产品数量,并打印产品信息(以数字替代)
*/
class MyConsumer extends Thread {
private PipedInputStream inputStream;
public MyConsumer(PipedInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
int count = inputStream.available();
if (count > 0) {
System.out.println("rest product count: " + count);
System.out.println("get product: " + inputStream.read());
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
总结
在并发编程中,经常需要实现线程通信,主要有如下几种方式:
1、使用volatile和synchronized关键字,这里的实现原理是使用Java内存模型的Happens-Before原则保证共享变量的可见性。
2、使用Object的wait()和notify/notifyAll实现线程等待和通知机制,需要结合锁实现。
3、使用管道的输入和输出流,基于内存进行线程通信