Java线程间通信的几种方式

Java编程思想中有这样一句话:

当我们使用线程来同时运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干扰到另外一个任务,这解决的是线程间彼此干涉的问题,现在我们需要来解决线程间彼此协调的问题,也就是线程间通信问题。

 

其实我一直对线程间通信这个概念比较模糊,如果仅仅从线程间相互协调来讲,只要不是单个线程孤立的独自执行,就都会涉及到线程间交互问题,都属于线程间通信的范畴。

  • join:一个线程会让另外一个线程加入进来执行完成,这里就涉及到协调。
  • 生产者/消费者模型:是生产者与消费者间的协调,两者间通过阻塞队列联系起来。
  • 线程工具类,如CountDownLatch,Semaphore等也涉及到线程协调。
  • 更广义一点的,线程中断,在一个线程中发起对另外一个线程的中断请求,另外一个线程响应中断,也涉及到协调。

以下是《Java编程思想》中提到的几种线程间通信的方式。

  • wait/notify/notifyAll
  • Lock和Condition
  • 管道
  • 阻塞队列

1.wait和notify/notifyAll

可以借助于Object类提供的wait()、notify()、notifyAll()三个方法,这三个方法属于Object类。但这三个方法必须由同步监视器对象来调用,这可分为两种情况
①对于用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
②对于用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

需要注意的是notify()方法只能随机唤醒一个线程,如果要唤醒所有线程,请使用notifyAll()。

/**
 * 来源;《Java编程思想》P709
 * 程序功能:一个餐厅有厨师和服务员。厨师准备好膳食后,通知服务员上菜,服务员上菜后等待。
 *
 * 餐厅
 */
public class Restaurant {
    Meal meal;
    ExecutorService exec = Executors.newCachedThreadPool();
    WaitPerson waitPerson = new WaitPerson(this);
    Chef chef = new Chef(this);
 
    public Restaurant() {
        //启动厨师和服务员任务
        exec.execute(chef);
        exec.execute(waitPerson);
    }
 
    public static void main(String[] args) {
        new Restaurant();
    }
}
 
/**
 * 餐
 */
class Meal {
    //订单号
    private final int orderNum;
 
    public Meal(int orderNum) {
        this.orderNum = orderNum;
    }
 
    @Override
    public String toString() {
        return "Meal " + orderNum;
    }
}
 
/**
 * 服务员
 */
class WaitPerson implements Runnable {
 
    private Restaurant restaurant;
 
    public WaitPerson(Restaurant r) {
        restaurant = r;
    }
 
    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                //服务员进入wait模式,直到被初始的notifyAll唤醒。
                synchronized (this) {//A
                    while (restaurant.meal == null) {
                        // 等待厨师生产
                        wait();
                    }
                }
 
                System.out.println("服务员上餐 " + restaurant.meal);
 
                synchronized (restaurant.chef) {//B
                    restaurant.meal = null;
                    //在锁上调用notifyAll
                    restaurant.chef.notifyAll(); // Ready for another
                }
            }
        } catch (InterruptedException e) {
            System.out.println("WaitPerson interrupted");
        }
    }
}
 
/**
 * 厨师
 */
class Chef implements Runnable {
 
    private Restaurant restaurant;
    private int count = 0;
 
    public Chef(Restaurant r) {
        restaurant = r;
    }
 
    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                //一直等待服务员收集到订单并通知厨师
                synchronized (this) {//C
                    while (restaurant.meal != null) {
                        //等待服务员通知
                        wait();
                    }
                }
 
                //模拟食材用尽的情况
                if (++count == 10) {
                    System.out.println("食材用尽, 餐厅打烊");
                    restaurant.exec.shutdownNow();
                }
 
                System.out.println("Order up! ");
 
 
                synchronized (restaurant.waitPerson) {//D
                    //开始烹饪
                    restaurant.meal = new Meal(count);
 
                    //在锁上调用notifyAll()
                    //通知服务员
                    restaurant.waitPerson.notifyAll();
                }
                TimeUnit.MILLISECONDS.sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println("Chef interrupted");
        }
    }
}

2.使用Lock和Condition

如果程序不用synchronized关键字来进行同步,而是用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
使用Lock对象的方式,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition将同步监视器方法wait()、notify()、notifyAll()分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法和同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法获得即可。Condition类提供了以下三个方法:
await():
signal():

signalAll():

3.使用阻塞队列控制线程通信

Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途不是作为容器,而是作为线程同步的工具。
BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满则线程被阻塞。而消费者线程 在取元素时,如果该队列已空则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

/**
 * 来源:《Java编程思想》P717的练习
 * 程序目的:用阻塞队列改写管道流的例子
 * 程序功能:写线程向阻塞队列添加字符,读线程从阻塞队列获取字符并打印到控制台,读取不到时阻塞。
 */
public class SendReceiveBQ {
 
    public static void main(String[] args) throws InterruptedException, IOException {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
 
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(sender);
        exec.execute(receiver);
 
        TimeUnit.SECONDS.sleep(4);
        exec.shutdownNow();
    }
 
}
 
class CharQueue extends LinkedBlockingQueue<Character> {
}
 
class Receiver implements Runnable {
 
    private CharQueue in;
 
    Receiver(Sender sender) {
        //使用同一个阻塞队列
        in = sender.getQueue();
    }
 
    @Override
    public void run() {
        try {
            while (true) {
                //阻塞,直到读取到字符
                System.out.println("Receiver:" + in.take());
            }
        } catch (InterruptedException e) {
            System.out.println(e + " Receiver interrupted");
        }
    }
}
 
class Sender implements Runnable {
 
    private Random random = new Random(47);
    private CharQueue out = new CharQueue();
 
    public CharQueue getQueue() {
        return out;
    }
 
    @Override
    public void run() {
        try {
            while (true) {
                for (char c = 'A'; c <= 'z'; c++) {
                    out.put(c);
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(500));
                }
            }
        } catch (InterruptedException e) {
            System.out.println(e + " Sender sleep interrupted");
        }
    }
}

4.使用管道流进行线程通信(被阻塞队列替代)

管道通信模型可以看成是生产者-消费者模型的变种,管道就是一个封装好的解决方案。管道基本上就是一个阻塞队列,所以可以用于线程间通信。管道在Java中对应的实现是PipedWriter类和PipedReader类。

但随着阻塞队列的出现,管道逐渐被替代。所以,在实际开发中,很少会使用到管道流。

下面是Java编程思想给出的管道应用的例子。了解更多:java管道流 PipedInputStream & PipedOutputStream 的应用

/**
 * 来源:《Java编程思想》P717
 * 程序目的:学习管道流的使用
 * 程序功能:写线程向管道写入字符,读线程从管道获取字符并打印到控制台,读取不到时阻塞。
 */
public class PipedIO {
 
    public static void main(String[] args) throws InterruptedException, IOException {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
 
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(sender);
        exec.execute(receiver);
 
        TimeUnit.SECONDS.sleep(4);
        exec.shutdownNow();
    }
}
 
class Receiver implements Runnable {
 
    private PipedReader in;
 
    Receiver(Sender sender) throws IOException {
        // PipedReader的创建需要与PipedWriter相连
        // 方式一:创建时就关联
        in = new PipedReader(sender.getPipedWriter());
 
        // 方式二:先创建,再关联
        // in = new PipedReader();
        // in.connect(sender.getPipedWriter());
    }
 
    @Override
    public void run() {
        try {
            while (true) {
                //阻塞,直到读取到字符
                System.out.println("Receiver:" + (char) in.read());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
 
class Sender implements Runnable {
 
    private Random random = new Random(47);
    private PipedWriter out = new PipedWriter();
 
    /**
     * Reader中的PipedReader需要与Sender中的PipedWriter相连,所以这里暴露方法
     */
    public PipedWriter getPipedWriter() {
        return out;
    }
 
    @Override
    public void run() {
        try {
            //循环
            while (true) {
                //输出A-z之间的字符
                for (char c = 'A'; c <= 'z'; c++) {
                    out.write(c);
                    //随机睡眠,让读线程有时间运行
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(500));
                }
            }
        } catch (IOException e) {
            System.out.println(e + " Sender write exception");
        } catch (InterruptedException e) {
            System.out.println(e + " Sender sleep interrupted");
        }
    }
}

 

posted @ 2018-05-29 17:40  静水楼台/Java部落阁  阅读(3284)  评论(0编辑  收藏  举报