java多线程之线程通信

Posted by 梧桐和风的博客 on May 11, 2017

在多线程机制中,线程之间需要传输信息。一般有以下几种通信机制:

  1. 共享对象:通过在共享对象中设置信号量,多个线程通过读取、修改该信号量来通信。
  2. wait/notify()方法:线程之间通过调用wait()、notify()方法实现线程等待、唤醒状态,从而达到线程通信的目的。

接下来我们分别看看这两种方法:

通过共享对象通信

在共享对象中设置信号量是最简单也是最常用的线程通信方法。共享变量需要使用volatile 修饰。我们知道,volatile 可以保证所修饰的变量立即被其他线程看到,而这种特性正是我们需要的。

下面是一个例子

public class TestShared {

    private   volatile  boolean flag =false;  //共享变量
    
    //用来保证主线程等待运行线程结束后再退出
    private CountDownLatch countDownLatch = new CountDownLatch(2);

    private   void test() {

        new Thread(() -> {
            for (int i=0;;i++) {
                if (flag) {  //当共享变量为true时,停止循环
                    break;
                }
                System.out.println(i+"-------->"+Thread.currentThread().getName());
            }
            countDownLatch.countDown();
        },"thread-one").start();

    }
    private void test2(){

        new Thread(() -> {
            try {
                Thread.sleep(3000);
                flag=true; //3秒后将共享变量设为true
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();

        },"thread-two").start();

    }

    public static void main(String[] args) {
        TestShared testShared = new TestShared();
        testShared.test();
        testShared.test2();
        try {
            testShared.countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

以上例子应该很好理解,利用共享变量flag 控制thread-one线程的停止。即完成了线程之间的通信。

这里需要说明的是,你会发现,即使你的共享变量没有用volatile修饰,线程one也会在规定时间停止。是的。volatile的作用是保证修饰变量对其他线程的可见性。但这并不表明,你不用volatile修饰,其他线程就看不到这个变量了。你最终都会看到这个变化,只是volatile会让你立马看到修改的结果。

有关volatile的介绍,请参看我的另一篇文章java内存模型与volatile详解

wait、notify()方法

java内置了一个等待机制实现线程之间的通信,在Object对象中有wait()、notify()、及notifyAll()3个方法。

在一个线程中调用了某对象wait()后,该线程就将处于等待状态。直到另一个线程调用同一对象的notify()才能继续运行。这样我们就可以据此实现线程通信。

另外需特别注意的是:为了调用某对象的wait、notify方法,你需要获取这个对象的锁。这是必须的。在实现上,JVM会检查调用wait 的线程是否同时是锁的拥有者,否则就抛出异常。

同样,我们来看一个例子:

public class WaitNotifyTest {
    /**
     * wait、notify用法
     * 当执行一个对象的wait()、notify()方法时,必须持有这个对象的锁,即在同步块中调用方法
     * 在JVM实现中,当执行一个对象的wait()方法时,会首先检查它是否在同步块中,否则抛出异常
     * @param args args
     */
    public static void main(String[] args) {
        NotifyObject notifyObject = new NotifyObject();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始");
            for(int i=0;i<100;i++){
                if(i==50){
                    //需获得这个对象的锁才能执行wait()方法
                    synchronized (notifyObject){
                        try {
                            notifyObject.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println( Thread.currentThread().getName()+":"+i);
            }

        }).start();
        new Thread(()->{
            for(int i=0;i<500;i++){
                System.out.println( Thread.currentThread().getName()+":"+i);
            }
            synchronized (notifyObject){
                notifyObject.notify();
            }
        }).start();
    }
}
class NotifyObject {
}

当第一个线程运行到i=50后,进入阻塞状态。直到第二个线程执行完毕,线程一才开始继续执行。

wait、notify机制要记得在执行该方法时,必须获得了该对象的锁。所以,为了使用方便,我们可以对其进行一定封装。

public class WaitNotify2Test {
    private final NotifyObject notifyObject = new NotifyObject();

    public void doWait() {
        synchronized (notifyObject) {
            try {
                notifyObject.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doNotify() {
        synchronized (notifyObject) {
            notifyObject.notify();
        }
    }

    public static void main(String[] args) {
       WaitNotify2Test waitNotify2Test = new WaitNotify2Test();

        new Thread(() -> {
            for(int i=0;i<100;i++){
                if(i==50){
                    waitNotify2Test.doWait();
                }
                System.out.println( Thread.currentThread().getName()+":"+i);
            }
        }).start();
        
        new Thread(()->{
            for(int i=0;i<500;i++){
                System.out.println( Thread.currentThread().getName()+":"+i);
            }
            waitNotify2Test.doNotify();
        }).start();
    }
}

NotifyObject类即是上面示例的内部类。通过对wait、notify的封装,我们可以使用更方便。

另外,还有一点需要注意:

不要在字符串常量或全局变量中调用wait、notify()方法。

这是因为,如果你的程序中有多套wait、notify线程,都使用一个字符串常量作为监视器的对象,则会出现假唤醒的情况。