使用ReentrantLock和Condition来代替内置锁和wait(),notify(),notifyAll()

本文深入探讨了ReentrantLock和Condition在Java并发编程中的应用,对比内置锁的wait()、notify()方法,展示了如何使用Condition实现更精细的线程间协作。通过具体示例,解释了Condition对象如何提供多个等待队列,以及如何在不同条件下唤醒特定线程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用ReentrantLock可以替代内置锁,当使用内置锁的时候,我们可以使用wait() nitify()和notifyAll()来控制线程之间的协作,那么,当我们使用ReentrantLock的时候,我们怎么来处理线程之间的写作呢?
JDK5.0为我们提供了Condition对象来替代内置锁的 wait(),notify()和notifyAll()方法

407044-20190412231507731-1210333974.jpg

内置锁的话,就只能有一个等待队列,所有的在某个对象上执行wait()方法的线程都会被加入到该对象的等待队列中去(线程会被挂起),需要其他的线程在同一个对象上调用notify()或者是notifyAll()方法来唤醒等待队列中的线程

而使用Condition的话,可以使用不同的等待队列,只需要使用lock.newCondition()即可定义一个Condition对象,每一个Condition对象上都会有一个等待队列(底层使用AQS),调用某个Condition对象的await()方法,就可以把当前线程加入到这个Condition对象的等待队列上

其他的线程调用同一个Condition对象的sinal()或者是signalAll()方法则会唤醒等待队列上的线程,使其能够继续执行

我们以一个现实中的例子来说明若何使用ReentrantLock和Condition如何替代synchronized和wait(),notify(),notifyAll():

我们模拟两个线程,一个线程执行登录操作,该登录操作会阻塞,然后等待另外一个线程将其唤醒(类似扫描登录的场景,页面会阻塞,等待扫码和确认,然后页面才会跳转)

首先是使用内置锁的例子:

package com.jiaoyiping.baseproject.condition;

import java.io.Serializable;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * Created with Intellij IDEA
 *
 * @author: jiaoyiping
 * Mail: jiaoyiping@gmail.com
 * Date: 2019/04/12
 * Time: 15:29
 * To change this template use File | Settings | Editor | File and Code Templates
 */

//使用内置锁来实现的等待/通知模型

public class LoginServiceUseInnerLock {
    private ConcurrentHashMap<String, Result> loginMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        LoginServiceUseInnerLock loginService = new LoginServiceUseInnerLock();
        String uuid = UUID.randomUUID().toString();
        System.out.println("[" + Thread.currentThread().getName() + "] 使用的UUID是: " + uuid);
        new Thread(() -> {
            loginService.login(uuid, 20_000);
            countDownLatch.countDown();
        }, "登录线程").start();
        Thread.sleep(2_000);
        new Thread(() -> {
            loginService.confirm(uuid);
            countDownLatch.countDown();
        }, "确认线程").start();
        countDownLatch.await();
        System.out.println("[" + Thread.currentThread().getName() + "] 两个线程都执行完毕了");

    }


    public void login(String code, int timeout) {
        Result result = new Result();
        result.setMessage("超时");
        loginMap.put(code, result);
        synchronized (result) {
            try {
                //超时的话,会自动返回,程序继续
                System.out.println("[" + Thread.currentThread().getName() + "] 登录线程挂起");
                result.wait(timeout);
                System.out.println("[" + Thread.currentThread().getName() + "] 登录线程继续执行,得到的结果是:" + result.getMessage());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                loginMap.remove(code);
            }
        }
    }

    public void confirm(String code) {
        assert code != null;
        Result result = loginMap.get(code);
        if (result == null) {
            System.out.println("[" + Thread.currentThread().getName() + "] 请求不存在或者已经过期");
            return;
        }
        result.setMessage("成功");
        synchronized (result) {
            //唤醒等待队列上的线程
            System.out.println("[" + Thread.currentThread().getName() + "] 确认线程开始唤醒阻塞的线程");
            result.notify();
        }

    }

    class Result implements Serializable {
        private static final long serialVersionUID = -4279280559711939661L;
        String message;

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public Result() {
        }
        public Result(String message) {
            this.message = message;
        }
    }

使用内置锁的时候,我们把random生成的key和一个自己定义的Result对象放置到ConcurrentHashMap中去,登录线程调用 Result对象的wait(timeout) 方法将当前线程挂起,并加入到Result对象的等待队列上去

确认线程根据key值,找到对应的Result对象,设置好message,然后调用Result对象的notify()方法唤醒等待队列上的线程,登录线程得以继续执行

那我们如何使用ReentrantLock和Condition来重写这个例子:

package com.jiaoyiping.baseproject.condition;

import java.io.Serializable;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with Intellij IDEA
 *
 * @author: jiaoyiping
 * Mail: jiaoyiping@gmail.com
 * Date: 2019/04/12
 * Time: 14:56
 * To change this template use File | Settings | Editor | File and Code Templates
 */

//使用ReentrantLock和Condition来实现的等待/通知模型

public class LoginServiceUseCondition {

    private ReentrantLock lock = new ReentrantLock();
    ConcurrentHashMap<String, Result> conditions = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        LoginServiceUseCondition loginService = new LoginServiceUseCondition();
        String uuid = UUID.randomUUID().toString();
        System.out.println("[" + Thread.currentThread().getName() + "] 使用的UUID是:" + uuid);
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
            loginService.login(uuid, 30_000);
            countDownLatch.countDown();
        }, "登录线程").start();
        Thread.sleep(5_000);
        new Thread(() -> {
            try {
                Thread.sleep(3_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            loginService.confirm(uuid);
            countDownLatch.countDown();
        }, "确认线程").start();
        countDownLatch.await();
        System.out.println("[" + Thread.currentThread().getName() + "] 两个线程都执行完毕了,退出");
    }


    /**
     * 过了超时时间之后,锁会自动释放
     *
     * @param code
     * @param timeout
     */
    public void login(String code, int timeout) {
        assert code != null;
        try {
            lock.tryLock(timeout, TimeUnit.MILLISECONDS);
            Condition condition = lock.newCondition();
            Result result = new Result("超时", condition);
            conditions.put(code, result);
            System.out.println("[" + Thread.currentThread().getName() + "] login()的请求开始阻塞");
            condition.await();
            System.out.println("[" + Thread.currentThread().getName() + "] 结束等待,继续执行,拿到的结果是" + result.getMessage());

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }

    //确认线程(拿这个UUID,去找到对应的Condition,唤醒上边的等待队列,并把Condition对象移除掉)

    public void confirm(String code) {
        assert code != null;
        Result result = conditions.get(code);
        Condition condition = result.getCondition();
        if (condition != null) {
            try {
                System.out.println("[" + Thread.currentThread().getName() + "] 找到对应的Condition对象,将其等待队列中的线程唤醒");
                lock.lock();
                result.setMessage("成功");
                condition.signal();
                conditions.remove(code);
            } finally {
                lock.unlock();
            }
        }
    }

    class Result implements Serializable {
        String message;
        final Condition condition;

        public Result(String message, Condition condition) {
            this.message = message;
            this.condition = condition;
        }

        public Result(Condition condition) {
            this.condition = condition;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public Condition getCondition() {
            return condition;
        }
    }

}    

上边的例子说明了怎么使用ReentrantLock和Condition来代替内置锁和wait(),notify(),notifyAll()

下边的一个来自jdk中的例子,演示了如何使用同一个ReentrantLock上的多个等待队列的情况

来自JDK文档中的示例(我稍加改造,加上了main方法和一些日志):

package com.jiaoyiping.baseproject.condition;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;

/**
 * Created with Intellij IDEA
 *
 * @author: jiaoyiping
 * Mail: jiaoyiping@gmail.com
 * Date: 2019/04/12
 * Time: 21:17
 * To change this template use File | Settings | Editor | File and Code Templates
 */

public class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

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

    public static void main(String[] args) throws InterruptedException {
        BoundedBuffer boundedBuffer = new BoundedBuffer();
        CountDownLatch countDownLatch = new CountDownLatch(40);
        //分别启动20个put线程和20个take线程

        IntStream.rangeClosed(1, 20).forEach(i -> {
            new Thread(() -> {
                try {
                    boundedBuffer.put(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }, "put线程 - " + i).start();
        });

        IntStream.rangeClosed(1, 20).forEach(i -> {
            new Thread(() -> {
                try {
                    boundedBuffer.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }, "take线程-" + i).start();
        });

        countDownLatch.await();
        System.out.println("[" + Thread.currentThread().getName() + "] 所有线程都执行完毕,退出");
    }

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            //put的线程,当队列满的时候挂起
            while (count == items.length) {
                System.out.println("[" + Thread.currentThread().getName() + "] 线程挂起");
                notFull.await();
            }
            Thread.sleep(1_000);
            items[putptr] = x;
            if (++putptr == items.length) {
                putptr = 0;
            }
            ++count;
            System.out.println("[" + Thread.currentThread().getName() + "] 执行完毕写操作,唤醒take线程");
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            //take的线程,当队列为空的时候,挂起
            while (count == 0) {
                System.out.println("[" + Thread.currentThread().getName() + "] 线程挂起");
                notEmpty.await();
            }
            Thread.sleep(1_000);
            Object x = items[takeptr];
            if (++takeptr == items.length) {
                takeptr = 0;
            }
            --count;
            System.out.println("[" + Thread.currentThread().getName() + "] 执行完毕读操作,唤醒put线程");
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}    

我们看到,以上代码中,使用到了两个Condition:notFull和notEmpty,都是通过lock对象的newCondition()方法得来的

items被放满之后,put的线程会在notFull的等待队列上进行等待(执行了notFull.await()方法) put线程执行完操作之后,会调用 notEmpty.signal()来试图唤醒在notEmpty上等待的线程(也就是给take线程发了一个信号,告诉它,items不是空的了,你可以过来take了)

当item空了之后,take线程会在notEmpty的等待队列上进行等待(执行了notEmpty的await()方法) 当take线程执行完操作之后,会调用notFull.signal()来唤醒在notFull上等待的线程(也就是给put线程发一个信号,告诉它,items不满了,你可以进行put操作了)

和内置方法类似,在调用await(),signal(),signalAll()等方法的时候,也必须要获得锁,也就是必须在 lock.lock()和lock.unlock()代码块儿之间才能调用这些方法,否则就会抛出IllegalMonitorStateException

转载于:https://www.cnblogs.com/jiaoyiping/p/10699273.html

<think>我们正在讨论Java中Object.wait()notifyAll()方法的使用方法注意事项。根据提供的引用,特别是引用[2][3],我们可以总结如下: 1. **基本用法**: - `wait()`: 使当前线程进入等待状态,直到其他线程调用该对象的`notify()`或`notifyAll()`方法,或者被中断。 - `notifyAll()`: 唤醒在该对象上等待的所有线程。注意,被唤醒的线程将竞争该对象的锁。 2. **使用前提**: - 这些方法必须在同步代码块或同步方法中调用,即当前线程必须持有该对象的锁(monitor),否则会抛出`IllegalMonitorStateException`[^2]。 - 通常,我们使用`synchronized`关键字来获取对象的锁。 3. **典型使用模式**: - 线程在检查某个条件不满足时,调用`wait()`释放锁并等待。 - 当另一个线程改变了条件,并调用`notify()`或`notifyAll()`来唤醒等待的线程。 - 被唤醒的线程会重新尝试获取锁,一旦获取成功,就从`wait()`返回并继续执行。 4. **注意事项**: - 使用`wait()`时,通常需要在循环中检查条件,因为被唤醒时条件可能仍未满足(虚假唤醒)[^3]。 - 使用`notifyAll()`通常比`notify()`更安全,因为它会唤醒所有等待的线程,避免某些线程永远不被唤醒的风险。但是,如果只需要唤醒一个线程,可以使用`notify()`。 5. **与Condition的对比**: - 在Java 1.5之后,引入了`Condition`接口,它提供了更灵活的线程间通信机制。`Condition`的`await()``signal()`/`signalAll()`方法类似于`wait()``notify()`/`notifyAll()`,但`Condition`可以绑定到多个等待条件上,使用更安全高效[^3]。 6. **示例场景**: - 生产者-消费者问题:当缓冲区满时,生产者等待;当缓冲区空时,消费者等待。通过`wait()``notifyAll()`协调生产消费。 下面是一个简单的示例,展示如何使用`wait()``notifyAll()`: ```java public class SharedObject { private boolean ready = false; public synchronized void doWait() throws InterruptedException { while (!ready) { // 循环检查条件,防止虚假唤醒 wait(); } // 执行条件满足后的操作 } public synchronized void doNotify() { ready = true; notifyAll(); // 唤醒所有等待线程 } } ``` 在这个例子中,线程调用`doWait()`方法时,如果条件(ready为true)不满足,就会等待。另一个线程调用`doNotify()`方法设置条件为true,并唤醒所有等待的线程。被唤醒的线程重新检查条件,如果满足则继续执行。 **注意事项**: - 在调用`wait()`方法时,会释放锁,以便其他线程可以获取锁并调用`notifyAll()`。 - 当线程被唤醒时,它需要重新获取锁才能从`wait()`返回。 **与Condition的对比**: 使用`Lock``Condition`可以更灵活地控制线程的等待唤醒。例如,我们可以创建多个条件(Condition)对象,让不同的线程等待不同的条件。而使用`Object`的监视器方法,所有的等待线程都在同一个条件上等待,无法区分。 例如,使用`Condition`实现生产者-消费者: ```java Lock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); // 生产者 lock.lock(); try { while (buffer.isFull()) { notFull.await(); // 等待不满 } buffer.add(item); notEmpty.signalAll(); // 唤醒消费者 } finally { lock.unlock(); } // 消费者 lock.lock(); try { while (buffer.isEmpty()) { notEmpty.await(); // 等待不空 } buffer.remove(); notFull.signalAll(); // 唤醒生产者 } finally { lock.unlock(); } ``` 这样,生产者消费者可以等待不同的条件,从而更高效地唤醒特定类型的线程。 总结: - 在需要线程间协调时,使用`wait()``notifyAll()`(或`notify()`)是基本方法,但要注意使用在同步块中,并在循环中检查条件。 - 在复杂场景下,推荐使用`Condition`,它提供了更精细的控制。 §§相关问题§§ 1. 什么是虚假唤醒(spurious wakeup)?如何避免? 2. 为什么使用`notifyAll()`比`notify()`更安全? 3. 在分布式系统中,如何实现类似`wait()``notifyAll()`的线程协调机制?</think>### Java 中 Object.wait() notifyAll()使用方法与注意事项 #### **一、核心机制** 1. **线程协作基础** - `wait()`: 使当前线程**释放锁并进入等待状态**,需被其他线程唤醒[^2][^3] - `notifyAll()`: **唤醒所有**在该对象监视器(monitor)上等待的线程[^3][^4] - 二者必须配合 `synchronized` 使用,否则抛出 `IllegalMonitorStateException`[^2] 2. **典型生产者-消费者模式** ```java class Message { private String content; private boolean empty = true; public synchronized String read() { while(empty) { // 必须用 while 循环检查条件 try { wait(); } // 释放锁并等待 catch (InterruptedException e) {} } empty = true; notifyAll(); // 唤醒所有等待线程 return content; } public synchronized void write(String value) { while(!empty) { // 循环检查条件 try { wait(); } catch (InterruptedException e) {} } content = value; empty = false; notifyAll(); // 通知所有读线程 } } ``` #### **二、使用步骤** 1. **获取对象锁** 在 `synchronized` 代码块中调用方法[^2]: ```java synchronized (sharedObject) { while(conditionNotMet) { // 必须用循环检查条件 sharedObject.wait(); // 释放锁并等待 } // ... 执行业务逻辑 sharedObject.notifyAll(); // 唤醒所有等待线程 } ``` 2. **多线程协调示例** ```java public static void main(String[] args) { Message message = new Message(); new Thread(new Reader(message), "R1").start(); // 读线程1 new Thread(new Reader(message), "R2").start(); // 读线程2 new Thread(new Writer(message), "W").start(); // 写线程 } ``` - 当写线程调用 `write()` 时,唤醒所有读线程[^4][^5] - 读线程通过 `while(empty)` 循环避免虚假唤醒 #### **三、关键注意事项** 1. **必须使用循环检查条件** ```java while(!condition) { // ❌ 错误:if (!condition) wait(); // ✅ 正确:while循环防止虚假唤醒 } ``` 虚假唤醒(spurious wakeup)可能导致条件未满足时线程被唤醒,需循环验证[^3]。 2. **notify() vs notifyAll()** | 方法 | 唤醒对象 | 适用场景 | |------|----------|----------| | `notify()` | 随机一个线程 | 单生产者-单消费者 | | `notifyAll()` | **所有等待线程** | 多生产者-多消费者(更安全)[^3][^4] | 3. **锁对象一致性** `wait()`/`notifyAll()` 必须使用**同一个对象锁**,否则导致监视器不匹配[^2]: ```java Object lock1 = new Object(); Object lock2 = new Object(); // ❌ 错误:不同锁对象 synchronized(lock1) { lock2.wait(); } ``` 4. **超时机制** 使用 `wait(long timeout)` 避免永久阻塞: ```java synchronized(lock) { if(!condition) { lock.wait(5000); // 最多等待5秒 } } ``` #### **四、Condition 的替代方案** Java 1.5+ 推荐使用 `Condition` 替代,提供更精细控制[^3]: ```java Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); try { while(!conditionMet) { condition.await(); // 类似 wait() } condition.signalAll(); // 类似 notifyAll() } finally { lock.unlock(); } ``` 优势:支持多个等待队列、可中断锁、公平锁等高级特性。 #### **五、典型应用场景** 1. **线程池任务协调** 主线程等待所有子任务完成(替代 `join()`) 2. **生产者-消费者队列** 缓冲区满时阻塞生产者,空时阻塞消费者 3. **资源初始化屏障** 确保所有依赖资源就绪后再启动主逻辑 > ⚠️ **重要原则**:永远在循环中调用 `wait()`,始终使用 `notifyAll()` 除非明确知道只需唤醒单个线程[^3][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值