一、案例需求

创建两个线程(t1 和 t2),交替输出数字 1-10:t1 输出 1,t2 输出 2,t1 输出 3,t2 输出 4…… 直至 10。

核心思路

线程通信的关键是 “协作”:一个线程执行完后,通知另一个线程执行,自己进入等待状态。具体依赖两个核心方法(定义在Object类中):

  • wait():释放持有的锁,进入等待状态,直到被notify唤醒(唤醒后需先重新竞争锁);
  • notifyAll():唤醒所有当前对象的等待线程。

通过这两个方法,可实现线程的 “交替执行”:t1 输出后唤醒 t2,自己等待;t2 输出后唤醒 t1,自己等待,循环往复。

二、代码实现与解析

完整代码

// 主类:启动两个线程
public class Main {
    public static void main(String[] args) {
        MyClass sharedObj = new MyClass(); // 共享对象(锁对象)
        Thread t1 = new Thread(new Processor(sharedObj), "t1");
        Thread t2 = new Thread(new Processor(sharedObj), "t2");
        t1.start();
        t2.start();
    }
}

// 共享类:包含交替输出的方法
public class MyClass {
    // t1执行的方法:输出奇数(1,3,5,7,9)
    public synchronized void print1() {
        for (int i = 0; i < 5; i++) { // 输出5次(1-10中的奇数)
            int num = 2 * i + 1; // 计算当前输出的奇数
            System.out.println("print1->" + num);
            
            this.notifyAll(); // 唤醒等待当前对象锁的线程(t2)
            try {
                this.wait(); // 释放锁,自己进入等待状态,等待t2唤醒。唤醒后需先重新竞争锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll(); // 循环结束后,唤醒可能等待的t2(避免死等)
    }

    // t2执行的方法:输出偶数(2,4,6,8,10)
    public synchronized void print2() {
        for (int i = 0; i < 5; i++) { // 输出5次(1-10中的偶数)
            int num = 2 * (i + 1); // 计算当前输出的偶数
            System.out.println("print2->" + num);
            
            this.notifyAll(); // 唤醒等待当前对象锁的线程(t1)
            try {
                this.wait(); // 释放锁,自己进入等待状态,等待t1唤醒。唤醒后需先重新竞争锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll(); // 循环结束后,唤醒可能等待的t1(避免死等)
    }
}

// 线程任务类:根据线程名调用不同方法
public class Processor implements Runnable {
    private MyClass sharedObj;

    public Processor(MyClass obj) {
        this.sharedObj = obj;
    }

    @Override
    public void run() {
        // t1调用print1,t2调用print2
        if ("t1".equals(Thread.currentThread().getName())) {
            sharedObj.print1();
        } else {
            sharedObj.print2();
        }
    }
}

代码解析

1. 共享对象与锁机制
  • MyClassprint1print2方法都用synchronized修饰,意味着它们的锁对象是MyClass的实例(sharedObj)。
  • 两个线程(t1 和 t2)共享同一个sharedObj,因此会竞争同一把锁,保证同一时间只有一个线程执行print1print2
2. 交替执行的核心逻辑

以 t1 和 t2 的第一次执行为例:

  • t1 先获取锁:执行print1,输出 “print1->1”;
  • t1 唤醒 t2:调用this.notifyAll(),唤醒等待sharedObj锁的 t2(此时 t2 从 “等待池” 进入 “锁池”);
  • t1 释放锁并等待:调用this.wait(),t1 释放锁并进入 “等待池”,等待被唤醒;
  • t2 获取锁:t2 从锁池竞争到锁,执行print2,输出 “print2->2”;
  • t2 唤醒 t1:调用this.notifyAll(),唤醒等待的 t1(t1重新尝试获取锁);
  • t2 释放锁并等待:调用this.wait(),t2 释放锁并进入等待池(t1获取锁);
  • 循环往复:t1 和 t2 交替获取锁,输出 3、4…… 直至 10。

三、wait()notifyAll()的使用规范

wait()notifyAll()是线程通信的核心,但使用时有严格规范,否则会导致IllegalMonitorStateException(非法监视器状态异常)。

1. 必须在同步块 / 同步方法中调用

wait()notifyAll()的调用者必须是 “当前持有锁的对象”,且必须在synchronized修饰的代码块或方法中使用。

错误示例(不在同步块中调用):

public void wrongMethod() {
    // 错误:未持有锁时调用wait()
    this.wait(); // 抛IllegalMonitorStateException
}

正确示例(在同步方法中调用):

public synchronized void rightMethod() {
    this.wait(); // 正确:当前线程持有this锁
}

2. wait()会释放锁,sleep()不会

  • wait():释放持有的锁,进入等待状态,直到被notify唤醒重新竞争锁;
  • sleep(long):不释放锁,线程进入阻塞状态,时间到后自动唤醒。

这是两者的核心区别,线程通信必须用wait()(需释放锁让其他线程执行)。

3. notify()notifyAll()的区别

  • notify():随机唤醒一个等待该锁的线程,让它竞争锁;
  • notifyAll():唤醒所有等待该锁的线程,让它们竞争锁。

实际开发中优先用notifyAll(),避免 “信号丢失”(如只唤醒一个线程但该线程不需要执行,导致其他线程永久等待)。

四、总结:线程通信的核心要点

  1. 锁是基础:线程通信必须基于同一把锁(共享对象),通过synchronized保证锁的唯一性;
  2. wait()释放锁等待:让当前线程释放锁并进入等待状态,给其他线程执行机会;
  3. notifyAll()唤醒协作:唤醒所有等待同一把锁的线程,让它们竞争锁继续执行;
  4. 避免死锁:循环结束后必须唤醒等待线程,用notifyAll()而非notify()减少信号丢失风险。

掌握线程通信机制可以更好理解如生产者 - 消费者模型、交替打印等并发程序。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐