java多线程(三)多线程通信 —— synchronized + wait + notifyAll等待唤醒机制


多线程间通信
多线程通信其实包含两方面,一个是线程间通知,一个线程告诉另一个线程是否结束。另一个是线程间传值。

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机制,将在下一章介绍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章