线程间通信
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。
线程通信的方式
一、共享内存
线程之间共享程序的公共状态来隐式通信。
1、volatile
volatile是具有可见性的:
- 当对volatile变量执行写操作后,JVM会把工作内存中的最新变量值强制刷新到主内存。
- 写操作会导致其他线程中的缓存无效。
- 线程使用缓存中变量时,先判断本地工作内存中此变量是否失效,若失效便从主内存中获取最新值。
例:
public class TestVolatile {
private static volatile boolean flag=true;
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (true){
if(flag){
System.out.println("线程A");
flag=false;
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true){
if(!flag){
System.out.println("线程B");
flag=true;
}
}
}
}).start();
}
}
执行结果:线程A和线程B交替执行
二、消息传递
线程之间通过发送信息来显示通信。
1、wait/notify等待通知方式
等待通知机制就是将处于等待状态的线程由其它线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。
最典型的例子就是生产者–消费者模式:
有一个产品队列,生产者想要在队列中添加产品,消费者需要从队列中取出产品。如果队列为空,消费者应该等待生产者添加产品后才进行消费;队列为满时,生产者需要等待消费者消费一部分产品后才能继续生产。此时就需要生产者和消费者这两个线程进行通信,通知对方此时应该做什么操作。
方法:
- wait()
当前线程释放锁并进入等待(阻塞)状态 - notify()
唤醒一个正在等待相应对象锁的线程,使其进入就绪状态 - notifyAll()
唤醒所有正在等待相应对象锁的线程,使其进入就绪状态
注意:
- 使用wait()、notify()、notifyAll()需要先加锁。
- 线程A调用wait(),线程B调用notify(),此时线程A进入就绪状态,等待线程B释放锁后争取锁的资源。
例:
public class TestWait {
// 队列最大size
private static Integer queueMxSize = 10;
// 队列
private static Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@SneakyThrows
public void run() {
while (true) {
//同步代码块,获取队列锁
synchronized (queue) {
if (queue.size() < queueMxSize) {
Thread.sleep(1000);
queue.offer(1);
System.out.println("生产者向队列中加入产品,产品数量为" + queue.size());
queue.notify();
} else {
System.out.println("队列已满,生产者停止生产,等待消费者消费");
queue.wait();
}
}
}
}
}, "producer").start();
new Thread(new Runnable() {
@SneakyThrows
public void run() {
while (true) {
//同步代码块,获取队列锁
synchronized (queue) {
if (queue.isEmpty()) {
System.out.println("队列为空,消费者停止消费,等待生产者生产");
queue.wait();
} else {
queue.poll();
System.out.println("消费者取出队列中的产品,产品数量为" + queue.size());
queue.notify();
}
}
}
}
}, "consumer").start();
}
}
执行结果:
2、join方式
在当前线程A调用线程B的join()方法后,会让当前线程A阻塞,直到线程B的逻辑执行完成,A线程才会解除阻塞,然后继续执行自己的业务逻辑。
join底层还是用了wait
例:
public class TestJoin {
@SneakyThrows
public static void main(String[] args) {
JoinThread aThread = new JoinThread("A");
JoinThread bThread = new JoinThread("B");
aThread.start();
aThread.join();
bThread.start();
}
static class JoinThread extends Thread{
private String name;
public JoinThread(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++){
System.out.println("线程" + name + "执行了" + i);
}
}
}
}
执行结果:
三、管道流
管道输入/输出流的形式。
java的管道的输入和输出实际上使用的是一个循环缓冲数组来实现的,默认为1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据,当这个缓冲数组已满的时候,输出流所在的线程就会被阻塞,当向这个缓冲数组为空时,输入流所在的线程就会被阻塞。
管道输入/输出流主要包括4种具体的实现:PipedOutputStream和PipedInputStream、PipedReader和PipedWriter,前两种面向字节,后两种面向字符。
注意:
对于Piped类型的流,必须先进性绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将抛出异常。
例:
public class TestPip {
@SneakyThrows
public static void main(String[] args) {
PipedWriter writer = new PipedWriter();
Print print = new Print();
PipedReader reader = print.getPipedReader();
//使用connect方法将输入流和输出流连接起来
reader.connect(writer);
writer.write("this is test");
writer.close();
Thread printThread = new Thread(print);
printThread.start();
}
static class Print implements Runnable {
private PipedReader reader = new PipedReader();
public PipedReader getPipedReader() {
return reader;
}
@SneakyThrows
@Override
public void run() {
char[] buf = new char[1024];
reader.read(buf);
System.out.println(new String(buf));
reader.close();
}
}
}
执行结果:
参考文献:拜托,线程间的通信真的很简单。