Java并发之线程间的协作

本文深入探讨了线程间的协作机制,重点介绍了wait/notify方法的应用,并通过生产者消费者问题加深理解。同时,还解析了join方法的实现原理。

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

     上篇文章我们介绍了synchronized关键字,使用它可以有效的解决我们多线程所带来的一些常见问题。例如:竞态条件,内存可见性等。并且,我们也说明了该关键字主要是一个加锁和释放锁的集成,所有为能获得锁的线程都将被阻塞在某个对象的阻塞队列上。而我们本篇将要介绍的线程间的协作则主要是对对象的另一个队列的使用(条件队列),所有因条件不满足而无法继续运行的线程都将在条件队列上进行等待。主要涉及内容如下:

  • 理解wait/notify这两个方法
  • 典型的生产者消费者问题
  • 理解join方法的实现原理

一、理解wait/notify这两个方法
     这两个方法是我们本篇文章的主角,它们被定义在根类Object中。

public final void wait()
public final native void wait(long timeout)

public final native void notify();
public final native void notifyAll();

两个wait方法,无参的wait相当于wait(0)表示无限期等待,有参数的wait方法则指定该线程等待多长时间。notify方法用于释放一个在条件队列上等待的线程,而notifyall方法则是用于释放所有在条件队列上进行等待的线程。那么究竟什么时候调用wait方法让线程到条件队列上去等待,什么时候调用notify释放条件队列上的线程呢?

我们说过一个对象有一把锁和两个队列,对于所有无法获取到锁的线程都将被阻塞在阻塞队列上,而对于获取到锁以后,于运行过程中由于缺少某些条件而不得不终止程序的线程将被阻塞在条件队列上并让出CPU。而且需要注意一点的是,线程被阻塞在阻塞队列上和条件队列上,所表现出的状态是不一样的。例如:

/*定义一个线程类*/
public class MyThread extends Thread{

    @Override
    public void run(){
        try {
            System.out.println(Thread.currentThread().getState());
            synchronized (this){
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/*启动线程*/
public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyThread();
        thread.start();

        Thread.sleep(1000);
        System.out.println(thread.getState());
        System.out.println("main is out");

    }

输出结果显示:

这里写图片描述

主函数中启动一个线程,该线程内部运行的时候先输出当前线程状态,然后调用wait方法将自己挂在当前线程对象的条件队列上并让出CPU,而我们在主函数中对该线程的状态进行再一次的输出, 从结果截图来看,程序并没有结束—-说明子线程并没有正常结束,阻塞在条件队列上的线程的状态是waiting,这和阻塞在阻塞队列上的线程状态blocked是完全两种不同的状态。但是,当我们调用notify或者notifyall方法将某个线程从条件队列中释放的时候,该线程要和外面的其他线程一样去竞争对象的锁,如果不能获取到对象的锁,依然会被阻塞在该对象的阻塞队列上。

二、使用wait/notify解决生产者消费者问题
     生产者消费者问题是我们操作系统中的一个经典的问题。生产者向仓库中源源不断的放入产品,消费者从仓库中源源不断的拿出产品,当仓库满的时候,生产者就不能继续往里面放入产品,当仓库空的时候,消费者就不能从仓库里取出产品。如何协调好生产者线程和消费者线程对仓库的操作就是这个问题的核心。

public class Repository {

    private ArrayDeque<String> list = null;
    private int limit;     //仓库容量

    public Repository(int limit){
        this.limit = limit;
        list = new ArrayDeque<String>(this.limit);
    }

    //仓库提供给生产者存入操作
    public synchronized void addGoods(String data) throws InterruptedException {
        while(list.size() == limit){
            //说明仓库已经满了
            wait();
        }
        list.add(data);
        System.out.println("i produce a product:"+data);
        notifyAll();
    }

    //仓库提供给消费者取出操作
    public synchronized String getGoods() throws InterruptedException {
        while(list.isEmpty()){
            //说明仓库已经空了
            wait();
        }
        String result = list.poll();
        System.out.println("i consume a product:"+ result);
        notifyAll();
        return result;
    }

}

我们定义一个仓库类,该仓库提供给生产者投放的方法,提供给消费者取出的方法。我们使用双端队列实现对仓库的模拟,limit参数限定仓库容量。

生产者的投放方法,当生产者想要向仓库投放产品时,如果仓库已经满了,则将将当前线程阻塞在条件队列上,等待仓库有空余位置为止。而如果仓库没满,则向其中投入一个产品并唤醒被阻塞在条件队列上的所有线程(在本例中实际上就是消费者线程)。一旦消费者线程从条件队列上被释放,他将重新和生产者线程竞争对象锁,在获取到对象锁之后将回到上次因条件不足而被阻塞的程序位置。消费者的取出方法和生产者的投放方法类似,此处不再赘述。

public class Producer extends Thread {

    //生产者线程不停的生产产品直到仓库满

    private Repository repository = null;

    public Producer(Repository r){
        this.repository = r;
    }

    int count = 0;
    @Override
    public void run(){
        while(true){
            try {
               repository.addGoods(String.valueOf(count));
                count++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定义一个生产者类,生产者始终不停的生产产品,我们用count来模拟产品代号。

public class Consumer extends Thread {

    //消费者线程不停的从仓库中取出产品直到仓库空

    private Repository repository = null;

    public Consumer(Repository r){
        this.repository = r;
    }

    @Override
    public void run(){
        while(true){
            try {
                String result = repository.getGoods();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定义一个消费者类,消费者不停的从仓库中取出产品。

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

        Repository repository = new Repository(20);
        Thread producer = new Producer(repository);
        Thread consumer = new Consumer(repository);

        producer.start();
        consumer.start();

        System.out.println("main thread is out");
    }

最后我们定义一个仓库并通过构造方法的传入使得生产者和消费者共同使用相同的仓库对象。分别启动两个线程,程序将死循环的输出生产者和消费者的生产和消费操作,以下是程序运行的部分结果:

这里写图片描述

我们可以看到生产者和消费者这两个线程交替的输出,偶尔会出现消费者滞后生产者的情况,但是消费者绝对不会超前生产者,因为只有生产者生产出产品之后,消费者才能取出。以上便是经典的生产者消费者问题,通过对该问题的实现,我们能够对wait/notify这两个操作有了一个更加深刻的认识。

三、join方法的实现原理
     join方法的内部其实使用的还是我们上述介绍的wait/notify机制。

public final void join() throws InterruptedException {
        join(0);
    }
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

两个方法,核心的还是这个带有参数的join方法。该方法大体上分为三种情况,如果millis小于0,抛出异常。如果millis等于0,就无限期等待,这一段代码不知道大家理解的如何:

if (millis == 0) {
    while (isAlive()) {
        wait(0);
    }
}
Thread thread = new MyThread();
thread.start();

thread.join();

两小段代码,第一段代码是jdk中关于millis 等于0的一个实现,第二段代码则是我们调用join方法的一个基本格式。我们可以看到,由于join这个方法被synchronized关键字修饰,那么我们主线程在调用thread对象的该方法时就需要首先获得thread对象的锁。

进入到join方法的内部,当millis 等于0的时候,判断只要线程对象活着,也就是thread对象活着,就调用wait(0)方法将当前线程(main)线程挂起到thread对象的条件队列上。一旦thread线程对象执行结束,Java系统将调用notifyall来释放所有挂在该对象的条件队列上的线程,此时main线程将会被唤醒,从而实现了main线程等待thread线程执行结束的一个过程。至于millis 大于0的情况,只不过内部调用了wait(long timeout)方法,其他的实现原理基本类似,此处不再赘述。

本篇文章,我们主要介绍线程间的一种协作机制,使用wait/notify两个方法来协作不同的线程。通过实现经典的生产者消费者模型增加了对wait/notify这两个方法的理解,最后从源代码的角度对Thread下的join方法进行了学习,该方法的核心就是利用wait/notify协作主线程和分支线程来实现等待的一个操作。总结不到之处,望指出。

<think>我们正在讨论Java并发编程中线程间通信的机制,特别是通过共享对象进行通信。用户之前询问了关于Condition的使用,现在转向更基础的共享对象通信机制。 根据引用[1]和[2],我们知道Java线程间通信有多种机制,其中共享变量是最基础的一种。同时,引用[2]详细介绍了wait/notify机制,这是使用共享对象进行通信的核心。 因此,我们的回答应该围绕以下要点: 1. 共享对象通信的基本原理:多个线程共享同一个对象,通过对该对象的成员变量进行读写来实现通信。 2. 需要同步机制(如synchronized)来保证线程安全。 3. 使用wait()、notify()、notifyAll()方法来实现线程间的协调(等待和唤醒)。 我们将通过一个具体的例子(生产者-消费者模型)来展示如何通过共享对象进行通信。 注意:根据用户要求,我们需要在回答中自然地添加引用标识,并在最后生成相关问题。</think>### Java并发编程中线程间通信:共享对象机制详解 在Java多线程编程中,**共享对象通信**是最基础的线程间通信方式,其核心原理是多个线程通过访问同一个对象的状态来实现信息交换。以下是关键机制和实现方法: #### 一、核心实现机制 1. **共享变量作为通信媒介** 多个线程通过读写同一个对象的成员变量传递信息: ```java public class SharedObject { private volatile String message; // 共享变量 public void setMessage(String msg) { this.message = msg; } public String getMessage() { return this.message; } } ``` 2. **同步控制保证可见性** 使用`synchronized`或`volatile`确保线程间状态可见性: - **`synchronized`** 保证原子性和可见性 - **`volatile`** 保证可见性(不保证原子性) ```java private volatile boolean flag = false; // volatile保证修改立即可见 ``` 3. **wait/notify协调执行顺序** 通过`Object`类的原生方法实现线程阻塞与唤醒: ```java synchronized (lock) { while (!condition) { lock.wait(); // 释放锁并等待 } // 执行业务逻辑 lock.notifyAll(); // 唤醒所有等待线程 } ``` #### 二、完整通信流程示例 ```java public class MessagePassing { private String message; private final Object lock = new Object(); private boolean hasNewMessage = false; // 生产者线程 public void produce(String msg) { synchronized (lock) { // 等待消费者取走消息 while (hasNewMessage) { try { lock.wait(); } catch (InterruptedException e) { /* 处理中断 */ } } message = msg; hasNewMessage = true; lock.notifyAll(); // 唤醒消费者 } } // 消费者线程 public String consume() { synchronized (lock) { // 等待新消息 while (!hasNewMessage) { try { lock.wait(); } catch (InterruptedException e) { /* 处理中断 */ } } String result = message; hasNewMessage = false; lock.notifyAll(); // 唤醒生产者 return result; } } } ``` **关键点解析**: 1. `while`循环检测条件,避免**虚假唤醒**(即使被意外唤醒仍会检查条件)[^2] 2. `notifyAll()`唤醒所有等待线程,确保不会永久阻塞 3. 同步块保护共享变量`message`和`hasNewMessage` 4. 通过`hasNewMessage`标志位实现精确状态控制 #### 三、共享对象通信的三大原则 1. **可见性原则** 共享变量必须用`volatile`修饰或通过`synchronized`访问,确保修改对其他线程立即可见。 2. **原子性原则** 复合操作(如`i++`)需在同步块中执行,避免竞态条件。 3. **有序性原则** 使用`happens-before`规则(如锁释放先于后续获取)保证操作顺序。 #### 四、适用场景与局限性 | **场景** | **优势** | **风险** | |-------------------------|-----------------------------------|------------------------------| | 简单状态通知 | 实现简单,无需额外依赖 | 复杂逻辑易出错 | | 生产者-消费者模型 | 天然匹配共享队列结构 | 需手动处理同步细节 | | 任务协调(如屏障同步) | 通过标志位灵活控制执行顺序 | 性能低于`java.util.concurrent`工具 | > **重要提示**: > - 永远在循环中调用`wait()`:`while(condition) { wait(); }`[^2] > - 优先使用`notifyAll()`:避免信号丢失导致线程永久阻塞 > - 同步块范围应最小化:减少锁竞争带来的性能损耗 #### 五、进阶替代方案 对于复杂场景,推荐使用更高级工具: 1. **`BlockingQueue`**:内置线程安全的队列通信机制 2. **`CountDownLatch`**:一次性屏障同步 3. **`CyclicBarrier`**:可重复使用的同步屏障 4. **`CompletableFuture`**:异步任务链式通信 > 共享对象通信是Java线程协作的基石,理解其原理对掌握`java.util.concurrent`高级工具至关重要[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值