Java并发编程基础-线程通信

在Java中各个线程是并行运行的,实际开发中经常需要在线程中实现通信,最经典的场景就是生产者-消费者机制;或者保证部分线程的执行顺序,比如让线程A等待直到线程B执行完成之后。本篇文章将通过一些示例逐步说明实现线程通信的几种方式(感慨一下,为了深入浅出写demo很耗精力,但是收获还是挺多,纸上得来终觉浅,绝知此事要躬行!)。

一、通过共享变量实现线程通信

之前在Java内存模型中,分析过volatile关键字的实现原理和使用方法,使用volatile修饰的共享变量,当一个线程对变量执行了写操作之后,其他线程会失效对共享变量的拷贝,从主存中获取最新的值。这个过程其实就实现线程间的通信。

如下栗子模拟了8只乌龟赛跑,其中有乌龟率先到达终点,通知其他乌龟比赛结束!

public class ThreadCommunication {

    private static final int MAX_LOOPS = 100;

    private static final int NUM_THREADS = 8;

    //使用volatile修饰共享变量,当某个线程修改了这个值后通知其他线程,从而实现线程间通信!
    private static volatile boolean gameover = false;
    private static volatile String winner;

    private static final Object lock = new Object();

    public static void main (String[] args) throws InterruptedException {

        Thread[] threads = new Thread[NUM_THREADS];

        for (int i=0; i

二、使用同步方式实现线程间通信

这个比较好理解,线程进入临界区需要获取锁,根据Java内存模型规定,同步块退出时的写对于后续同步块入口的读可见,所以可以通过在同步块中修改公共属性的值实现线程间的通信。

public class ThreadCommunication2 {

    private static int count;

    public static void main (String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Syncs(), "Thread1");
        Thread t2 = new Thread(new Syncs(), "Thread2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("In main thread, count = " + count);
    }

    static final class Syncs implements Runnable {

        @Override
        public void run() {
            synchronized (ThreadCommunication2.class) {
                count++;
                System.out.println("in thread " + Thread.currentThread().getName() + ", count = " + count);
            }
        }
    }

}
运行结果:
in thread Thread1, count = 1
in thread Thread2, count = 2
In main thread, count = 2

三、基于wait和notify、notifyall方式实现生产者-消费者模式

需要注意的是,wait和notify都依赖于锁。
具体流程:线程A执行wait方法之前必须获得锁,当执行wait方法之后线程A主动释放锁,线程B获取锁之后,执行notify/notifyAll方法时,会将线程A从锁的等待队列中移到同步队列中,当线程B退出同步块释放锁的时候,线程A重新竞争锁。
notify和notifyAll的区别就是notify是把一个线程从等待队列移入同步队列,而notifyAll是锁对象上等待队列所有线程都移入同步队列。


Java并发编程基础-线程通信_第1张图片
线程等待-通知模式流程图.png

图片说明:实线箭头是线程A的流程,虚线箭头是线程B的流程。

如下展示了一个简单的栗子,模拟一个工厂生产和销售面包过程

public class BreadFactory {

    /**
     * 使用无界队列保存产品作为仓库
     */
    private static final ConcurrentLinkedQueue breads = new ConcurrentLinkedQueue();

    /**
     * 仓库保存产品的最大个数,当仓库中的产品个数超过这个时,生产者等待.
     */
    private static final int MAX_BREADS_NUM = 100;
    /**
     * 仓库保存产品的最小个数,当仓库中产品个数比这个少时,消费者等待.
     */
    private static final int MIX_BREAD_NUM = 50;

    /**
     * 线程通过wait和notify/notifyAll通信时必须配合锁
     */
    private static final Object lock = new Object();

    public static void main(String[] args) {

        Thread producer = new Thread(new Producer(), "Producer");
        Thread consumer = new Thread(new Consumer(), "Consumer");
        //启动生产者和消费者线程
        producer.start();
        consumer.start();

    }
    //生产者线程
    protected static final class Producer implements Runnable{
        @Override
        public void run() {
            synchronized (lock) {
                while (true) {
                    while (breads.size() == MAX_BREADS_NUM) {
                        try {
                            //生产者进行等待.
                            System.out.println("Producer is waiting..");
                            lock.wait();
                            //生产者被唤醒,重新竞争锁
                            System.out.println("Producer is wakeup..");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    Bread bread = new Bread();
                    breads.add(bread);
                    System.out.println(String.format("Produce bread " + bread.getId()));
                    //生产者通知消费者可以执行消费了。
                    lock.notifyAll();
                }
            }
        }
    }
    //消费者线程
    protected static final class Consumer implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                while (true) {
                    while (breads.size() <= MIX_BREAD_NUM) {
                        try {
                            //消费者等待
                            System.out.println("Consumer is waiting..");
                            lock.wait();
                            //此时消费者被唤醒,重新竞争锁
                            System.out.println("Consumer is wakeup..");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Bread bread = breads.poll();
                    System.out.println(String.format("Consume bread " + bread.getId()));
                    //消费产品后通知生产者
                    lock.notifyAll();
                }
            }
        }
    }
    //产品实体类
    protected static final class Bread {

        private long id;

        public Bread() {
            super();
            //此处简单地随机生成一个id
            id = new Random().nextLong();
        }

        public long getId() {
            return id;
        }

        public void setId(long id) {
            this.id = id;
        }
    }

}

四、管道输入/输出流

管道输入/输出和普通的文件输入/输出流或者网络输入/输出流不同之处在于它主要用于线程间的数据传输,而传输的媒介是内存。管道输入/输出流主要包含4个具体实现:PipedOutputStream、PipedInputStream,PipedRead、PipedWriter,前两者面向字节,后两者面向字符。
核心思想就是一个线程往流里写数据,另外一个线程从流里读数据。

如下栗子是我偷懒从网上复制并本地执行的,因为很简单所以我没有再写了。
有个地方需要注意,需要调用connect将输入流和输出流进行连接!

public class PipeTest1 {

    public static void main(String[] args) {

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream();
        try {
            //将输入和输出流连接,重要!!
            pis.connect(pos);
        } catch (IOException e) {
            e.printStackTrace();
        }

        new MyProducer(pos).start();
        new MyConsumer(pis).start();

    }
}
class MyProducer extends Thread {

    private PipedOutputStream outputStream;

    private int index = 0;

    public MyProducer(PipedOutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void run() {
        while (true) {
            try {
                for (int i = 0; i < 5; i++) {
                    outputStream.write(index++);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 消费者每0.5秒从管道中取1件产品,并打印剩余产品数量,并打印产品信息(以数字替代)
 */
class MyConsumer extends Thread {

    private PipedInputStream inputStream;

    public MyConsumer(PipedInputStream inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                int count = inputStream.available();
                if (count > 0) {
                    System.out.println("rest product count: " + count);
                    System.out.println("get product: " + inputStream.read());
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

总结
在并发编程中,经常需要实现线程通信,主要有如下几种方式:
1、使用volatile和synchronized关键字,这里的实现原理是使用Java内存模型的Happens-Before原则保证共享变量的可见性。
2、使用Object的wait()和notify/notifyAll实现线程等待和通知机制,需要结合锁实现。
3、使用管道的输入和输出流,基于内存进行线程通信

你可能感兴趣的:(Java并发编程基础-线程通信)