在并发编程中,我们需要处理两个关键问题:
1. 线程之间如何通信
通信是指线程之间以何种机制来交换信息,在命令式编程(c语言)中,线程之间的通信机制有两种:
1.1 共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
JAVA并发采用的是共享内存模型,java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
1.2 消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来进行通信
2. 线程之间如何同步
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
2.1 在共享内存并发模型里,同步是显示进行的
2.2 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式的
共享变量 = 实例域(对象)、静态域和数组元素
共享变量存储在堆内存中,而堆内存在线程之间共享
局部变量,方法定义参数,异常处理参数不会在线程之间共享
它们不会有内存可见性问题,也不受内存模型的影响
java线程之间的通信由JMM控制
JMM决定一个线程对共享变量的写入何时对另一个线程可见
MM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存中
每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本,本地内存是JMM的一个抽象概念,并不真实存在。
它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
JMM的抽象示意图:
从上图来看,线程A和线程B之间要通信的话,需要经历下面2个步骤:
1. 线程A把本地内存中更新过的共享变量刷新到主内存中去
2. 线程B到主内存中去读取线程A之前已更新过的共享变量
JMM通过控制主内存与每个线程的本地内存之间的交互来为java程序员提供内存可见性保证
JAVA还有种方式可以用于线程间通信——管道流
操作和普通IO并无二致,只是需要把管道连接起来
PipedOutputStream内部使用PipedInputStream接收需要输出的字节
用管道流实现生产者和消费者模式:
生产者:
public class Producer implements Runnable {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
@Override
public void run() {
try {
pos.write("Hello World".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
消费者:
public class Consumer implements Runnable {
private PipedInputStream pis;
public Consumer(PipedInputStream pis) {
this.pis = pis;
}
@Override
public void run() {
// 将数据保存在byte数组中
byte[] bytes = new byte[100];
try {
// 从数组中得到实际大小。
int length = pis.read(bytes);
System.out.println(new String(bytes, 0, length));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试类:
public class TestPipedStream {
public static void main(String[] args) {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
try {
// 连接管道
pos.connect(pis);
new Thread(new Producer(pos)).start();
new Thread(new Consumer(pis)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行可以看到Hello World