Java并发编程(九)线程间协作(下)

上篇我们讲了使用wait()和notify()使线程间实现合作,这种方式很直接也很灵活,但是使用之前需要获取对象的锁,notify()调用的次数如果小于等待线程的数量就会导致有的线程会一直等待下去。这篇我们讲多线程间接协作的方式,阻塞队列和管道通讯,间接协作的优点是使用起来更简单并且不易出错。

阻塞队列

阻塞队列提供了一种功能,即你可以在任何时刻向队列内扔一个对象,如果队列满了则当前线程阻塞;在任何时刻都可以从队列中取出一个对象,如果队列为空则当前线程阻塞。阻塞队列是线程安全的,使用它时无需加锁。此外其内部是使用显示锁实现的同步,使用Condition实现的线程阻塞。阻塞队列的接口是BlockingQueue,它有两个实现类:

1. ArrayBlockingQueue:底层使用数组实现的队列,有固定长度,调用其构造方法时必须提供队列的最大长度。

2. LinkedBlockingQueue:底层使用链表实现的队列,理论上讲是没有最大长度的,使用时不用提供队列长度;但实际上这个队列的长度不能超过Integer.MAX_VALUE。

这两个类使用的时候没有太大区别,我们以LinkedBlockingQueue为例,重写“学生去食堂打饭”的例子,代码如下:

class Student implements Runnable {
    private Object wan = new Object();
    public void run() {
        try {
            System.out.println("学生:取到了一个碗");
            BlockingQueueTest.wanQueue.put(wan);
            System.out.println("学生:阿姨帮忙盛饭");
            wan = BlockingQueueTest.wanWithFanQueue.take();
            System.out.println("学生:吃饭");
        } catch (InterruptedException e) {}
    }
}
class CafeteriaWorker implements Runnable {
    public void run() {
        try {
            Object wan = BlockingQueueTest.wanQueue.take();
            System.out.println("阿姨:给学生盛饭");
            BlockingQueueTest.wanWithFanQueue.put(wan);
        } catch (InterruptedException e) {}
    }
}
public class BlockingQueueTest {
    public static BlockingQueue wanQueue = new LinkedBlockingQueue();
    public static BlockingQueue wanWithFanQueue = new LinkedBlockingQueue();
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new Student());
        exec.execute(new CafeteriaWorker());
        exec.shutdown();
    }
}

输出结果如下:

学生:取到了一个碗

学生:阿姨帮忙盛饭

阿姨:给学生盛饭

学生:吃饭

在这个例子中我们定义了两个队列,一个是空碗的队列,另一个是盛完饭的碗的队列。“学生线程”取到碗后将空碗放入wanQueue队列,然后试图从wanWithFanQueue队列中取出盛好的饭碗;“阿姨线程”试图从wanQueue队列中取出空碗,然后将盛好的饭碗放到wanWithFanQueue队列中。上次我们使用wait()方法时必须要求“阿姨线程”先启动,否则会导致“阿姨线程”错过学生的信号,而使用阻塞队列实现时我们就不再要求两个线程的启动顺序了,使用阻塞队列规避了错失信号的风险。有的同学可能会好奇为什么会使用两个队列,这是因为如果使用同一个队列,同学线程把碗扔进队列后,可能“阿姨线程”没来得及取出来就被“同学线程”拿回去了,感兴趣的同学可以自行测试。

管道通讯

通过管道的方式也可以使线程间实现交互,管道和阻塞队列类似,当管道内没有数据的时候,如果某个线程尝试去读取数据就会被阻塞。

我们可以使用PipedWriter和PipedReader来实现对管道数据的读取和写入。和阻塞队列不同的是,阻塞队列中不同线程都是操作一个队列的对象;使用管道时,不同的线程可以使用不同的对象,只要将它们注册为一个管道即可。

我们使用管道通信模拟一个线程对另一个线程表白,代码如下:

class Sender implements Runnable {
    private PipedWriter writer;
    Sender(PipedWriter writer) {
        this.writer = writer;
    }
    public void run() {
        String str1 = new String("I love you\n");
        String str2 = new String("Do you love me\n");
        try {
            writer.write(str1.toCharArray());
            writer.write(str2.toCharArray());
        } catch (IOException e) {}
    }
}
class Receiver implements Runnable {
    private PipedReader reader;
    public Receiver(PipedReader reader) {
        this.reader = reader;
    }
    public void run() {
        try {
            while(true) {
                char c = (char)reader.read();
                System.out.print(c);
            }
        } catch (IOException e) {}
    }
}
public class PipeCommunication {
    public static void main(String[] args) throws Exception {
        PipedReader reader = new PipedReader();
        PipedWriter writer = new PipedWriter(reader);
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new Sender(writer));
        exec.execute(new Receiver(reader));
                Thread.sleep(1000);
        exec.shutdownNow();
    }
}

运行后输出结果如下,一秒后程序退出:

I love you

Do you love me

我们在主方法里先定义了一个PipedReader对象,然后将这个对象作为PipedWriter的构造方法的参数传给PipedWriter对象,这样就实现两个输入输出流的绑定,分别将两个流对象传给两个线程对象。在信息的接收方我们使用一个死循环让其不断的从管道内读入,从输出结果可以看出read()方法在管道内没有数据的时候被阻塞了,因为输出结果没有循环打印其它字符。此外主线程sleep一秒后调用了shutdownNow()方法,这个方法向所有运行着的线程发送中断信号,程序运行一秒后就退出了,我们可以看出中断信号打断了Receiver的阻塞状态,由此得出结论:管道类阻塞时可以被中断信号打断。

总结

本篇讲了使用阻塞队列和管道来实现线程间的合作,相对于使用wait()协作而言这两种方式更为高级,使用起来更容易而且不易错,此外阻塞队列和管道都是线程安全的,因此使用它们的时候不需要使用锁。需要实现线程间协作时可以根据实际需要,权衡利弊进行选择。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

原文地址:https://www.cnblogs.com/victorwux/p/9218358.html

时间: 2024-07-28 18:18:42

Java并发编程(九)线程间协作(下)的相关文章

19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对

Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态.然后等待消费者消费了商品,然后消费者通知生产者队列有空间了.同样地,当

Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

Java并发编程系列[未完]: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) 一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线程创建完成时为新

【转】Java并发编程:线程池的使用

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool

Java并发编程:线程池的使用(转)

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool

JAVA并发编程3_线程同步之synchronized关键字

在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题. 不安全线程分析 public class Test implements Runnable { private int i = 0; private int getNext() { return i++; } @Override public void run() { // synchronized while (true) { synchro

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

Java并发编程:线程的同步

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发编程:线程的创建

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发编程-一个线程的内心独白

最近正在学习Java并发编程实践,无意中发现了这篇文章,特别的有意思,而且覆盖的知识点也很多,忍不住分享给大家! 我是一个线程, 我一出生就被编了个号: 0x3704, 然后被领到一个昏暗的屋子里, 这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长, 他带着沧桑的口气对我说: 我们线程的宿命就是处理包裹. 把包裹处理完以后还得马上回到这里,否则可能永远回不来了. 我一脸懵懂,包裹,什么包裹? "不要着急,马上你就会明白了, 我们这里是不养闲人的." 果然,