• 线程间通信的几种方式


    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编程思想》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");
            }
        }
    }

    总结:

    1.线程间通信的几种方式?有什么区别?

    ①.wait/notify/notifyAll

    ②.Lock和Condition

    ③.使用管道进行IO

    ④.阻塞队列

    wait/notify/notifyAll和Lock和Condition一样,是一种比较底层的线程间通信方式,区别在于前者适用于使用内置锁(synchronized)场景,而后者适用于使用显式加锁(Lock)场景。另外,使用管道来实现线程间通信的方式只存在老版本的java版本中,在新的java版本中引入了阻塞队列替换了它。所以通常来说,如果要实现线程间通信,可以直接使用阻塞队列。

    2.阻塞队列的作用?有哪几种不同的实现版本?

  • 相关阅读:
    第13章 使用ADO.NET访问数据库
    第11章 连接查询和分组查询
    第10章 模糊查询和聚合函数
    第9章 数据查询基础
    数据库前三章测试题
    用表组织数据
    程序数据集散地:数据库
    深入C#的String类
    线程池
    hadoop-2.8.0 完全分布式运行模式
  • 原文地址:https://www.cnblogs.com/rouqinglangzi/p/9106298.html
Copyright © 2020-2023  润新知