Java 线程间通信和协作的两种方式

现在计算机和智能手机都是多核处理器,为了更好地发挥设备的性能,提高应用程序的体验性,多线程是必不可少的技术。线程之间不是孤立的,它们共享进程的资源和数据,彼此之间还需要进行通信和协作,最典型的例子就是「生产者-消费者模型」。下面先介绍 wait/notify 机制和 Lock/Condition 机制,然后用两个线程交替打印奇偶数。

1. wait/notify

wait 和 notify 是 Object 类的两个方法,理解起来还是有些复杂的。它和多线程同步有关系,个人觉得放在 Object 类不太合理,可能是历史遗留问题吧。每个对象都有一把锁(monitor),在进入同步方法或代码块之前,当前线程需要先获取对象锁,然后才能执行同步块的代码,完成后释放对象锁。锁可以理解为唯一的凭证,有了它就能入场,而且独占所有的资源,立场就得交出去。

wait 方法的作用是使当前线程释放对象锁,并进入等待状态,不再往下执行。当其他线程调用对象的 notify/notifyAll 时,会唤醒等待的线程,等到其他线程释放锁后,被唤醒的现象将继续往下执行。notify 随机唤醒一个等待的线程,notifAll 唤醒所有等待的线程。注意:wait 和 notify 都需要在拿到对象锁的情况下调用。下面是 wait 的标准使用方法(来自 《Effective Java》一书):

synchronized (obj) {
  while (condition does not hold) {
    obj.wait(); // release lock and reacquire on wakeup
    // perform action appropriate to condition
  }
}

每个锁对象都有两个队列:就绪队列和阻塞队列。就绪队列存储了已经就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当阻塞线程被唤醒后,才会进入就绪队列,然后等待 CPU 的调度;反之,当一个线程被阻塞后,就会进入阻塞队列,等待被唤醒。

举个例子,线程 A 在执行任务,它等待线程 B 做完某个操作,才能往下执行,这就可以用 wait/notify 实现。

    public void start() {
        new Thread(new TaskA()).start();
        new Thread(new TaskB()).start();
    }

    private final Object lock = new Object();
    private boolean finished;

    private class TaskA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("线程 A 拿到锁了,开始工作");
                while (!finished) {
                    try {
                        System.out.println("线程 A 释放了锁,进入等待状态");
                        lock.wait();
                        System.out.println("线程 A 收到信号,继续工作");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("线程 A 释放了锁");
        }
    }

    private class TaskB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("线程 B 拿到了锁,开始工作");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("-----------------------");
                System.out.println("线程 B 发信号了,完成工作");
                finished = true;
                lock.notify();
            }
            System.out.println("线程 B 释放了锁");
        }
    }
/* 打印:
线程 A 拿到锁了,开始工作
线程 A 释放了锁,进入等待状态
线程 B 拿到了锁,开始工作
-----------------------
线程 B 发信号了,完成工作
线程 B 释放了锁
线程 A 收到信号,继续工作
线程 A 释放了锁  
*/

2. Lock/Condition

Condition 可以看作 Object 的 wait/notify 的替代方案,同样用来实现线程间的协作。与使用 wait/notify 相比,Condition的 await/signal 更加灵活、安全和高效。Condition 是个接口,基本的方法就是 await() 和 signal()。Condition 依赖于 Lock 接口,生成一个 Condition 的代码是 lock.newCondition() 。 需要注意 Condition 的 await()/signal() 使用都必须在lock.lock() 和 lock.unlock() 之间才可以,Conditon 和 Object 的 wait/notify 有着天然的对应关系:

  • Conditon 中的 await() 对应 Object 的 wait();
  • Condition 中的 signal() 对应 Object 的 notify();
  • Condition 中的 signalAll() 对应 Object 的 notifyAll();

举个例子,使用 Condition 实现和上面的功能。

    public void start() {
        new Thread(new TaskC()).start();
        new Thread(new TaskD()).start();
    }
    
    private Lock reentrantLock = new ReentrantLock();
    private Condition condition = reentrantLock.newCondition();

    private class TaskC implements Runnable {
        @Override
        public void run() {
            reentrantLock.lock();
            System.out.println("线程 C 拿到了锁,开始工作");
            try {
                System.out.println("线程 C 释放了锁,进入等待状态");
                condition.await();
                System.out.println("线程 C 收到信号,继续工作");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("线程 C 释放了锁");
                reentrantLock.unlock();
            }
        }
    }

    private class TaskD implements Runnable {
        @Override
        public void run() {
            reentrantLock.lock();
            System.out.println("线程 D 拿到了锁,开始工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("-----------------------");
            try {
                System.out.println("线程 D 发信号了,完成工作");
                condition.signal();
            } finally {
                System.out.println("线程 D 释放了锁");
                reentrantLock.unlock();
            }
        }
    }
/*打印:
线程 C 拿到了锁,开始工作
线程 C 释放了锁,进入等待状态
线程 D 拿到了锁,开始工作
-----------------------
线程 D 发信号了,完成工作
线程 D 释放了锁
线程 C 收到信号,继续工作
线程 C 释放了锁
*/

相比 Object 的 wait/notify,Condition 有许多优点:

  • Condition 可以支持多个等待队列,因为一个 Lock 实例可以绑定多个 Condition

  • Condition 支持等待状态下不响应中断

  • Condition 支持当前线程进入等待状态,直到将来的某个时间

3. 两个线程交替打印奇偶数

使用 wait/notify:

    public void printNumber() {
        new Thread(new EvenTask()).start();
        new Thread(new OddTask()).start();
    }
    
    private int number = 10;
    private final Object numberLock = new Object();

    private class EvenTask implements Runnable {
        @Override
        public void run() {
            synchronized (numberLock) {
                while (number >= 0 && (number & 1) == 0) {
                    System.out.println("偶数: " + (number--));
                    numberLock.notify();
                    try {
                        numberLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private class OddTask implements Runnable {
        @Override
        public void run() {
            synchronized (numberLock) {
                while (number >= 0 && (number & 1) == 1) {
                    System.out.println("奇数: " + (number--));
                    numberLock.notify();
                    try {
                        numberLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

使用 Lock/Condition:

    public void printNumber() {
        new Thread(new EvenTask()).start();
        new Thread(new OddTask()).start();
    }
    
    private int number = 10;
    private Condition evenCondition = reentrantLock.newCondition();
    private Condition oddCondition = reentrantLock.newCondition();

    private class EvenTask implements Runnable {

        @Override
        public void run() {
            reentrantLock.lock();
            try {
                while (number >= 0 && (number & 1) == 0) {
                    System.out.println("偶数: " + (number--));
                    oddCondition.signal();
                    evenCondition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    private class OddTask implements Runnable {
        @Override
        public void run() {
            reentrantLock.lock();
            try {
                while (number >= 0 && (number & 1) == 1) {
                    System.out.println("奇数: " + (number--));
                    evenCondition.signal();
                    oddCondition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }

运行后打印:

偶数: 10
奇数: 9
偶数: 8
奇数: 7
偶数: 6
奇数: 5
偶数: 4
奇数: 3
偶数: 2
奇数: 1
偶数: 0

最后,建议使用 Lock/Condition 代替 Object 的 wait/notify,因为前者是 java.util.concurrent 包下的接口,对于同步更简洁高效,多线程操作优先选用 JUC 包的类。

参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容