java 线程通信

823 阅读4分钟

2.5 线程通信

不像进程间内存资源是独立的,各线程间共享地址和数据空间。因此,最简单的通信方式就是在一个线程中更新全局变量的值,在另一个线程中,通过无限循环去测试这个值,但这样无疑是很浪费处理器资源的。

2.5.1 传统线程通信

在一开始,java就支持了线程,线程间通过wait/notify/notifyAll通信,属于互斥量的形式。

/**
 * 阻塞当前线程,直到另一个线程调用了该对象的notify或notifyAll方法
 */
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
  • 调用wait方法后,当前线程释放所持有的该对象的锁,进入等待队列,等待其他线程调用对象的notify或notifyAll方法后,重新进入就绪队列。即暗指该线程首先得拥有该对象的锁。

  • wait/notify/notifyAll必须在同步方法/同步块中调用,使用while而不是if测试条件变量

    // Thread-1
    synchronized(mutex) {
    		while(!condition){ 
    	    	try {
              	mutex.wait();
          	} catch (InterruptedException e) {
              	e.printStackTrace();
          	}
    		}
    		System.out.println("thread-1 business");
    }
    
    // Thread-2
    synchronized(mutex) {
    	if(!condition){ 
    			// do something
    	    condition = true;
    	    mutex.notifyAll();
    	}
    }
    

    1).如果没有放在同步块中,mutex.wait()调用释放锁时没有持有锁,会抛出java.lang.IllegalMonitorStateException,如果在同步块中,当进入同步块中时,肯定持有了对象的锁。

    2).再者,如果没有同步,当线程1进入while循环块,调用wait之前,线程2执行了condition = true; mutex.notifyAll();,最后线程1调用wait,则线程1无法再有机会被唤醒,会一直在阻塞状态

/** 在等待队列中随机唤起一个线程 */
public final native void notify();
/** 唤起所有等待队列中的线程 */
public final native void notifyAll();
  • 调用notify/notifyAll的线程必须持有锁

  • 调用notify/notifyAll不会释放锁,如果在Thread-2中加入while (true) ;,则Thread-1无法被唤醒了

    synchronized (mutex) {
       if (!condition) {
          // do something
          condition = true;
          mutex.notifyAll();
          while (true) ;
    	 }
    }
    

下面,看一道面试题:

/**
 * 下面代码打印值多少?,取消注释后打印值多少?
 */
public class WaitThread {

    public static void main(String[] args) {
        CalcThread calcThread = new CalcThread();
        calcThread.start();

        synchronized (calcThread) {
//            try {
//                calcThread.wait();
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

            System.out.println("Total is: " + calcThread.total);
        }
    }

    static class CalcThread extends Thread {
        int total;

        @Override
        public void run() {
            synchronized (this) {
                for (int i = 0; i < 100; i++) {
                    total += i;
                }
                notify();
            }
        }
    }
}

2.5.2 基于Condition线程通信

​ 在jdk1.5版中,引入了java.util.concurrent工具包,将原来对象上的wait/notify/notifyAll同步方法功能从Object独立出来,通过Condition(await/signal/SignalAll)提供另一种选择。

​ 相较于wait/notify/notifyAll + synchronized,Condition使用await/signal/SignalAll + Lock实现相同甚至更灵活的功能。先看一个Condition源码注释文档里一个例子:

public class BoundedBuffer {

    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
          	//使用while,防止虚假唤醒(spurious wakeup)问题
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}
  • Condition关联一个Lock(基于CAS),而不是monitor实现,所以可以在一个同步实现中构造多个Condition(等待集)
  • await()调用时释放lock并使用LockSupport.park(this);阻塞当前线程,等被唤醒后重新获取lock
  • signal()调用时先校验是否持有lock,然后从条件等待队列中唤醒一个线程(重新进入同步队列竞争锁),最后调用signal()的方法退出并释放锁(见take之finally

2.5.3 基于阻塞队列(BlockingQueue)线程通信

​ 基于线程安全的队列也可以实现线程间通信,常用的安全队列有:

ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue
DelayQueue

​ 阻塞队列BlockingQueue的实现和上面的BoundedBuffer差不多,同步队列SynchronousQueue采用自旋+CAS+LockSupport实现。

关于阻塞队列,以后再详细介绍

2.5.4 PipedInputStream/PipedOutputStream

线程间可以通过管道的方式通信,原理是通过一个缓冲buffer交换数据,通过同步方法connect建立管道连接。

public class PipedStreamTest {

    private static PipedReader reader = new PipedReader();
    private static PipedWriter writer = new PipedWriter();

    static {
        try {
            reader.connect(writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            IntStream.range(0, 10).forEach(i -> {
                try {
                    System.out.println("read from pipe: " + reader.read());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }, "reader").start();

        new Thread(() -> {
            IntStream.range(0, 10).forEach(i -> {
                try {
                    writer.write(i);
                    System.out.println("write to pipe: " + i);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }, "writer").start();
    }
}
  • 不要在同一个线程中同时使用PipedInputStream和PipedOutputStream,会造成死锁
  • 不要同时从输入流connect到输出流,又反向连接
  • 缓冲区为空或满时,会阻塞当前线程