java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java线程间的通信

Java并发线程间的通信最佳实践

作者:左左右右左右摇晃

这篇文章主要介绍了Java并发线程间的通信最佳实践,本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作,需要的朋友可以参考下

在多线程编程中,线程间通信是一个核心话题。当多个线程需要协同完成某个任务时,它们必须能够互相通知状态的变化,以避免竞态条件和无效的资源占用。Java提供了多种线程间通信的方式,从最基础的 wait/notify 机制,到 Lock 配合 Condition 的灵活方案。本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作。

一、线程间通信的必要性

思考一个简单的场景:两个线程操作一个共享变量,一个线程负责加1,另一个线程负责减1,要求交替执行10轮。如果没有通信机制,线程A可能连续加多次,线程B才减一次,导致结果混乱。线程间通信正是为了解决这类问题——让线程在合适的时机暂停和唤醒,从而保证操作的顺序性和数据的一致性。

二、传统的wait/notify机制

2.1 基本使用

Java中每个对象都有一组监视器方法:wait()notify()notifyAll()。它们必须在同步块(synchronized)中使用,因为需要获取对象的监视器锁。

下面是一个经典的“生产者-消费者”示例,两个线程交替对变量进行+1和-1操作:

class ShareData {
    private int number = 0;
    public synchronized void increment() throws InterruptedException {
        // 1. 判断
        if (number != 0) {
            this.wait();
        }
        // 2. 干活
        number++;
        System.out.println(Thread.currentThread().getName() + " => " + number);
        // 3. 通知
        this.notifyAll();
    }
    public synchronized void decrement() throws InterruptedException {
        if (number != 1) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + " => " + number);
        this.notifyAll();
    }
}
public class WaitNotifyDemo {
    public static void main(String[] args) {
        ShareData data = new ShareData();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }, "B").start();
    }
}

运行结果会交替输出 A => 1 和 B => 0,共10轮。这里的关键点在于:

2.2 虚假唤醒问题

当我们将线程数增加到4个(两个加线程,两个减线程),并运行多次后,可能会看到 23 等异常值,甚至出现负数。这是因为 if 判断导致的虚假唤醒

虚假唤醒指的是线程被唤醒后,条件可能已经不再满足,但程序仍然继续执行。例如,当 number 为0时,A1和A2都等待在 increment 方法中;当B执行减1后调用 notifyAll(),A1和A2同时被唤醒,它们都从 wait() 后继续执行,导致 number 被连续加了两次,变为2。

解决方案:将 if 改为 while,使线程被唤醒后重新检查条件。这是JDK文档明确要求的。

public synchronized void increment() throws InterruptedException {
    while (number != 0) {  // 使用while
        this.wait();
    }
    number++;
    System.out.println(Thread.currentThread().getName() + " => " + number);
    this.notifyAll();
}

2.3 wait/notify的局限性

三、Lock + Condition:更灵活的通信方式

从JDK 1.5开始,java.util.concurrent.locks 包提供了 Lock 接口和 Condition 接口,弥补了 wait/notify 的不足。

3.1 Condition的基本用法

每个 Condition 对象都相当于一个“队列”,通过 await() 和 signal()/signalAll() 实现线程的等待与唤醒。与 wait/notify 类似,使用前必须先获取对应的锁。

将上面的例子用 ReentrantLock 和 Condition 改写:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
    private int number = 0;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

相比 synchronizedLock 提供了更多控制能力(如 tryLock、可中断锁等),而 Condition 则可以创建多个等待队列,实现精确唤醒

3.2 多个Condition实现精准通信

需求:三个线程 A、B、C 依次执行,A 打印5次,B 打印10次,C 打印15次,循环10轮。

这种场景下,需要在线程A执行完后精确唤醒B,B执行完后精确唤醒C,C执行完后精确唤醒A。通过为每个线程创建一个 Condition 对象,并结合一个状态标识,即可轻松实现。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareResource {
    private int flag = 1; // 1: A, 2: B, 3: C
    private final Lock lock = new ReentrantLock();
    private final Condition conditionA = lock.newCondition();
    private final Condition conditionB = lock.newCondition();
    private final Condition conditionC = lock.newCondition();
    public void print5() {
        lock.lock();
        try {
            while (flag != 1) {
                conditionA.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 2;
            conditionB.signal(); // 唤醒B
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print10() {
        lock.lock();
        try {
            while (flag != 2) {
                conditionB.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 3;
            conditionC.signal(); // 唤醒C
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15() {
        lock.lock();
        try {
            while (flag != 3) {
                conditionC.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 1;
            conditionA.signal(); // 唤醒A
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ConditionDemo {
    public static void main(String[] args) {
        ShareResource resource = new ShareResource();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print5();
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print10();
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print15();
        }, "C").start();
    }
}

这样,每个线程只会在属于自己的标识位被设置时才执行,执行完后精确唤醒下一个线程,避免了无效的唤醒竞争。

四、经典面试题:交替打印数字和字母

题目:两个线程,一个打印1~52的数字,另一个打印A~Z的字母,要求打印结果为12A34B...5152Z。

分析:数字线程每次打印两个数字,字母线程每次打印一个字母。可以通过一个标志位来控制切换,也可以用 Condition 来实现精确交替。

4.1 使用 wait/notify 实现

class Printer {
    private int num = 1;
    private char letter = 'A';
    private boolean printNum = true;
    public synchronized void printNumber() {
        for (int i = 0; i < 26; i++) {
            while (!printNum) {
                try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            System.out.print(num++);
            System.out.print(num++);
            printNum = false;
            notifyAll();
        }
    }
    public synchronized void printLetter() {
        for (int i = 0; i < 26; i++) {
            while (printNum) {
                try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            System.out.print(letter++);
            printNum = true;
            notifyAll();
        }
    }
}
public class PrintDemo {
    public static void main(String[] args) {
        Printer printer = new Printer();
        new Thread(printer::printNumber).start();
        new Thread(printer::printLetter).start();
    }
}

4.2 使用 Condition 实现

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Printer {
    private int num = 1;
    private char letter = 'A';
    private boolean printNum = true;
    private final Lock lock = new ReentrantLock();
    private final Condition numberCondition = lock.newCondition();
    private final Condition letterCondition = lock.newCondition();
    public void printNumber() {
        lock.lock();
        try {
            for (int i = 0; i < 26; i++) {
                while (!printNum) {
                    numberCondition.await();
                }
                System.out.print(num++);
                System.out.print(num++);
                printNum = false;
                letterCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printLetter() {
        lock.lock();
        try {
            for (int i = 0; i < 26; i++) {
                while (printNum) {
                    letterCondition.await();
                }
                System.out.print(letter++);
                printNum = true;
                numberCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

五、总结与最佳实践

到此这篇关于Java并发线程间的通信最佳实践的文章就介绍到这了,更多相关Java线程间的通信内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文