文章目录
- 1. java线程间直接传值
- java也提供不同线程间传值
- 2. 等待、唤醒机制
- synchronized + wait + notify
- 如何使用共享对象:
- 生产者消费者模型:例子说明
- 生产消费者模型:安全问题
- 生产消费者模型:加同步锁
- 生产消费者模型:等待唤醒机制
- 小结
多线程间通信
多线程通信其实包含两方面,一个是线程间通知,一个线程告诉另一个线程是否结束。另一个是线程间传值。
1. java线程间直接传值
而java多线程之间的通信他不像Erlang语言那种没有共享变量,每个线程都操作单独变量然后传递值给其他线程。java这种操作共享变量机制就要加同步锁来解决一系列安全问题。
所以说Erlang在操作多线程更安全是很有道理的。
java也提供不同线程间传值
PipedInputStream类 与 PipedOutputStream类 用于在应用程序中创建管道通信。一个PipedInputStream实例对象必须和一个PipedOutputStream实例对象进行连接而产生一个通信管道。
在 Java 的 JDK 中,提供了四个类用于线程间通信传值:
- 字节流:PipedInputStream 和 PipedOutputStream;
- 字符流:PipedReader 和 PipedWriter;
PipedOutputStream可以向管道中写入数据,PipedIntputStream可以读取PipedOutputStream向管道中写入的数据,这两个类主要用来完成线程之间的通信。一个线程的PipedInputStream对象能够从另外一个线程的PipedOutputStream对象中读取数据,如下图所示:
但感觉实际上用到的情况比较多的是线程操作数据而不是储存数据,所以数据都存储在主线程内存中,其他线程来操作就可以了。
2. 等待、唤醒机制
等待唤醒机制主要是一个线程通知其他线程是否来进入操作。
JDK5之前主要实现方式是使用synchronized 和notify 、wait方法。但是存在一些缺陷,JDK5是一个改动很大的版本,之后使用Lock锁来保证同步 和await、notify方法来实现代替之前版本。
我们通过java多线程范例生产者消费者模式来介绍这两种方式。
synchronized + wait + notify
之前我们的例子比如说银行存钱,每个线程都是执行相同的代码,都是向银行中存钱。
而通常情况我们是要不同的线程执行不同的代码做不同的事,比如说一个线程往银行存钱,一个线程从银行取钱。我们现在用一个经典的生产者消费者例子来介绍多线程。
如何使用共享对象:
首先我们介绍一下如何使用共享对象,这里我们使用共享对象的时候可以这样做:这里相当于把主线程的变量传递给子线程,子线程就可以修改该变量。
//自定义共享对象
Res r = new Res();
//定义两个线程对象把共享对象传入构造方法
Input in = new Input(r);
Output out = new Output(r);
//开启线程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
生产者消费者模型:例子说明
我们生产面包,生产1消费1,生产2消费2,也就是说我们的生产和消费要一一对应。并且生产和消费在不同线程执行。
而且每生产一个就要消费一个这样不造成资源的浪费。也就是说生产和消费一一对应。
生产消费者模型:安全问题
//共享资源类
class Resource {
private int breadCount = 0; // 资源编号
// 生产资源
public void produce(String name) {
synchronized (this) {
breadCount++; // 资源编号递增,用来模拟资源递增
System.out.println(Thread.currentThread().getName() + "...生产者生产bread.." + breadCount);
}
}
// 消费资源
public void consume() {
System.out.println(Thread.currentThread().getName() + "...消费者消费bread......." + breadCount);
}
}
// 生产者类线程
class Producer implements Runnable {
private Resource res;
//构造函数中生产者初始化分配资源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.produce("bread"); // 循环生产10次
}
}
}
// 消费者类线程
class Comsumer implements Runnable {
private Resource res;
//构造函数中消费者一初始化也要分配资源
public Comsumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.consume(); // 循环消费10次
}
}
}
public class SynchronizedSample {
public static void main(String[] args) {
Resource resource = new Resource(); // 实例化资源
Producer producer = new Producer(resource); // 实例化生产者和消费者类,它们取得同一个资源
Comsumer comsumer = new Comsumer(resource);
Thread threadProducer = new Thread(producer); // 创建1个生产者线程
Thread threadComsumer = new Thread(comsumer); // 创建1个消费者线程
// 分别开启线程
threadProducer.start();
threadComsumer.start();
}
}
/*
Thread-0...生产者生产..bread1
Thread-0...生产者生产..bread2
Thread-0...生产者生产..bread3
Thread-1...消费者消费.......bread1
Thread-1...消费者消费.......bread4
Thread-1...消费者消费.......bread4
...*/
我们可以发现由于我们没有加任何同步机制由于使用共享资源,多线程所以必然会出现安全问题,这里同时消费多个bread4
生产消费者模型:加同步锁
由于上述问题,我们需要解决,因为我们在生产资源的时候,比如生产bread2,这时只有生产线程在操作,消费线程是不能操作的。也就是说保证生产一个面包的时候只有一个线程在操作该面包。
我们查找哪块共享资源是多线程要操作,然后修改:
// 资源类
class Resource {
private int breadCount = 0; // 资源编号
// 生产资源
public void produce(String name) {
synchronized (this) {
breadCount++; // 资源编号递增,用来模拟资源递增
System.out.println(Thread.currentThread().getName() + "...生产者生产bread.." + breadCount);
}
}
// 消费资源
public void consume() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "...消费者消费bread......." + breadCount);
}
}
}
/*
Thread-0...生产者生产bread..1
Thread-0...生产者生产bread..2
Thread-0...生产者生产bread..3
Thread-0...生产者生产bread..4
Thread-0...生产者生产bread..5
Thread-0...生产者生产bread..6
Thread-0...生产者生产bread..7
Thread-0...生产者生产bread..8
Thread-0...生产者生产bread..9
Thread-0...生产者生产bread..10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10
Thread-1...消费者消费bread.......10*/
我们在共享资源的地方添加了同步块,也就是说,当我们每生产一个面包的时候,不能消费该面包。但是又出现个问题就是我们的生产者线程:
// 生产者类线程
class Producer implements Runnable {
private Resource res;
//构造函数中生产者初始化分配资源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.produce("bread"); // 循环生产10次
}
}
}
在生产者线程中循环10次调用produce同步锁,而一直没退出该线程,只有当退出该线程时才会释放同步锁,而我们一直没有退出该线程就没释放该锁,锁只有退出该线程时才会释放。所以生产者线程一直拿着这个锁生产了10个bread而消费者不能消费,等到生产完10个才进行消费,这不是我们目标,我们要求生产一个就消费一个,不然之前生产的面包可能会过期。
生产消费者模型:等待唤醒机制
为解决上述问题,我们可以为资源设置一个标志flag,该标志用来标明资源是否存在,所有的线程执行操作前都要判断资源是否存在。系统初始化后,资源是空的。如果是消费者线程获得执行权,先判断资源,此时为空,就会进入冻结状态,交出执行权,并唤醒其他线程。当生产者线程获得执行权,先判断资源,若为空,立马进行生产,生产完成进入冻结交出执行权并唤醒其他线程。
线程间通知:通过等待唤醒实现:
wait()让线程进入冻结状态,交出执行权(释放锁)
notify() 唤醒一个冻结状态的线程(持有相同锁的线程)
notifyAll() 唤醒所有冻结状态线程(持有相同锁的)
这样的方法都用在同步里,因为需要锁,用锁的对象来调用这些方法,比如这个锁是r,那么就使用r.wait().表示持有r这个锁的线程。所以只有同一个锁上的被等待线程可以被同一个锁的r.notify()唤醒。不能唤醒持有其他所的线程。
但是如果有多个线程,比如多个生产线程和多个消费线程,notifyAll()会全部唤醒他们。遗憾的是,并不能直接唤醒对方线程比如只唤醒消费者。这点也就是他的缺陷,也就是后面一章我们讲的JDK5的版本升级,对synchronized和wait模式的替换。
// 资源类
class Resource {
private int breadCount = 0; // 资源编号
private boolean flag = false; // 资源类增加一个资源标志位,判断是否有资源
// 生产资源
public void produce() {
synchronized (this) {
// 添加循环判断,如果flag为true,也就是有资源了,生产者线程就暂停生产,进入冻结状态,等待唤醒。
while (flag == true) {
try {
this.wait(); // wait函数抛出的异常只能被截获
// 因为wait会让该线程等在这里,如果这里使用if判断,则当线程被唤醒后会直接往下执行,
// 不再进行flag判断了,则由于错误标记执行,这就可能造成多线程死锁。
//所以使用while循环判断,让线程再次判断是否标记正确
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当可以生产的时候flag=false时,执行以下
breadCount++; // 资源编号递增,用来模拟资源递增
System.out.println(Thread.currentThread().getName() + "...生产者生产bread.." + breadCount);
//生产完成修改flag
flag = true;
//并唤醒其他所有线程,如果只唤醒一个,则可能唤醒一个同类线程,而我们要唤醒的是对方线程
this.notifyAll();
}
}
// 消费资源
public void consume() {
synchronized (this) {
//判断如果没有资源则消费者等待
while (flag == false) {
try {
this.wait(); // wait函数抛出的异常只能被截获
// 因为wait会让该线程等在这里,如果这里使用if判断,则当线程被唤醒后会直接往下执行,
// 不再进行flag判断了,则由于错误标记执行,这就可能造成多线程死锁。
//所以使用while循环判断,让线程再次判断是否标记正确
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "...消费者消费bread......." + breadCount);
//生产完成修改flag
flag = false;
//并唤醒其他所有线程,如果只唤醒一个,则可能唤醒一个同类线程,而我们要唤醒的是对方线程
this.notifyAll();
}
}
}
// 生产者类线程
class Producer implements Runnable {
private Resource res;
//构造函数中生产者初始化分配资源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
res.produce(); // 每个线程生产5个
}
}
}
// 消费者类线程
class Comsumer implements Runnable {
private Resource res;
//构造函数中消费者一初始化也要分配资源
public Comsumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
res.consume(); // 每个线程消费5个
}
}
}
public class SynchronizedSample {
public static void main(String[] args) {
Resource resource = new Resource(); // 实例化资源
Thread threadProducer = new Thread(new Producer(resource)); // 创建2个生产者线程
Thread threadProducer2 = new Thread(new Producer(resource)); // 创建2个生产者线程
Thread threadComsumer = new Thread(new Comsumer(resource)); // 创建2个消费者线程
Thread threadComsumer2 = new Thread(new Comsumer(resource)); // 创建2个消费者线程
// 分别开启线程
threadProducer.start();
threadProducer2.start();
threadComsumer.start();
threadComsumer2.start();
}
}
/*
Thread-0...生产者生产bread..1
Thread-2...消费者消费bread.......1
Thread-1...生产者生产bread..2
Thread-3...消费者消费bread.......2
Thread-0...生产者生产bread..3
Thread-2...消费者消费bread.......3
Thread-1...生产者生产bread..4
Thread-3...消费者消费bread.......4
Thread-0...生产者生产bread..5
Thread-2...消费者消费bread.......5
Thread-1...生产者生产bread..6
Thread-3...消费者消费bread.......6
Thread-0...生产者生产bread..7
Thread-2...消费者消费bread.......7
Thread-1...生产者生产bread..8
Thread-2...消费者消费bread.......8
Thread-0...生产者生产bread..9
Thread-3...消费者消费bread.......9
Thread-1...生产者生产bread..10
Thread-3...消费者消费bread.......10
*/
根据结果可以看出:不同线程负责生产和消费,当生产一个就消费一个。运行正确。
进入 wait()方法后,当前线程释放锁。然后当我们notifyAll()唤醒其他线程的时候,谁竞争到这个对象锁谁就获得执行权,进行执行,执行完后再进入wait()等待并释放锁,让其他线程执行。
上述代码中的问题有2点需要注意,
- 第一点是用if还是while来判断flag,这点在代码注释中已经说明,
- 第二点是用notify还是notifyAll函数。也在代码注释中说明
所以,多线程一般都要用while和notifyAll()的组合。
小结
多线程编程往往是多个线程执行不同的任务,不同的任务不仅需要“同步”,还需要“等待唤醒机制”。两者结合就可以实现多线程编程,其中的生产者消费者模式就是经典范例。
然而,使用synchronized修饰同步函数和使用Object类中的wait,notify方法实现等待唤醒是有弊端的。就是效率问题,notifyAll方法唤醒所有被wait的线程,包括本类型的线程,如果本类型的线程被唤醒,还要再次判断并进入wait,这就产生了很大的效率问题,也在代码中给出了说明。理想状态下,生产者线程要唤醒消费者线程,而消费者线程要唤醒生产者线程。为此,JDK5提供了Lock和Condition接口及实现类来替代sychronized和wait机制,将在下一章介绍。