java线程通信 生产者消费者,synchronized,,ReentrantLock,Condition(笔记备份)

1. 线程通信

简单案例:

两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。

线程间通信模型:

  1. 生产者+消费者
  2. 通知等待唤醒机制

多线程编程模板:

线程 操作 资源类

资源类 判断, 干活, 通知

代码实现:

java 复制代码
//资源类
class Cache {
    private int num = 0;

    // 生产
    public synchronized void produce() {
        //这里故意使用if进行判断
        if (num != 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        //干活
        num++;
        System.out.println(Thread.currentThread().getName() +"生产了一个,现在的数量是:" + num);


        //通知
        notifyAll();
    }

    //消费
    public synchronized void consume() {
        //判断
        if (num != 1) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        //干活
        num--;
        System.out.println(Thread.currentThread().getName() +"消费了一个,现在的数量是:" + num);


        //通知
        notifyAll();
    }
}


public class DemoWaitNotify {
    public static void main(String[] args) {
        Cache cache = new Cache();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.produce();
            }
        }, "生产者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.consume();
            }
        }, "消费者1").start();
    }
}

notify() 只随机唤醒一个 正在该对象监视器上 wait() 的线程,而 notifyAll() 会唤醒所有在那儿等待的线程(至于谁最终拿到锁继续跑,还是要抢)。

如果换成4个线程会怎样?

java 复制代码
public class DemoWaitNotify {
    public static void main(String[] args) {
        Cache cache = new Cache();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.produce();
            }
        }, "生产者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.consume();
            }
        }, "消费者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.produce();
            }
        }, "生产者2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.consume();
            }
        }, "消费者2").start();
    }
}

出现了不期望的结果

2. 虚假唤醒

换成4个线程会导致错误,虚假唤醒

  • 你如果用的是 if (条件不满足) wait();
    那么醒来以后不会再判断一次条件 ,就直接往下跑,所以会出事(尤其是虚假唤醒 、或被 notifyAll 叫醒但条件仍不满足)。
  • 你如果用的是 while (条件不满足) wait();
    那么醒来以后会回到 while 的条件判断处再检查一次 ,不满足就继续 wait(),这才是正确姿势。

线程从 wait() 返回后从原位置继续执行;为了保证条件仍成立,必须用 while 重新判断等待条件,不能用 if

解决虚假唤醒:查看API,java.lang.Object的wait方法

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html#wait()

if换成while

java 复制代码
// 1. 判断
while (num != 0) {
    this.wait();
}

再次测试,完美解决

3. 线程通信(Condition)

对标synchronized:

并提供了实现案例:

官网:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Condition.html

Lock 替代 synchronized;Condition 替代 wait/notify/notifyAll;并且 Condition 让你能有"多个等待队列"。

使用Condition实现线程通信,改造之前的代码:

java 复制代码
// 多线程编程模版
// 线程操作资源类 , 资源类 判断 ,干活, 通知


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//资源类
class Cache {
    private int num = 0; //0 : empty   1: full
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition(); //生产者等待/被唤醒的条件队列 。  现在不满了,你可以生产了
    private final Condition notEmpty = lock.newCondition();  // 消费者等待/被唤醒的条件队列   现在不空了,你可以消费

    // 生产
    public void produce() throws InterruptedException {
        lock.lock();
        //判断
        try {
            while (num != 0) { //说明有东西, 当前不需要生产
                notFull.await();
            }
            //干活
            num++;
            System.out.println(Thread.currentThread().getName()+"生产了一个,现在的数量是:" + num);


            //通知
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    //消费
    public void consume() throws InterruptedException {
        lock.lock();
        try {
            //判断
            while (num != 1) { //说明没有东西可以消费
                notEmpty.await();
            }

            //干活
            num--;
            System.out.println(Thread.currentThread().getName() +"消费了一个,现在的数量是:" + num);
            notFull.signal();
        } finally {
            //通知
            lock.unlock();
        }


    }
}


public class DemoWaitNotify {
    public static void main(String[] args) {
        Cache cache = new Cache();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    cache.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    cache.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    cache.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    cache.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者2").start();
    }
}
java 复制代码
condition.signal(); // 唤醒一个等待的线程    
condition.signalAll();  //唤醒所有等待的线程

两个条件是为了把"生产者等待"和"消费者等待"拆成两个独立等待队列,从而精准唤醒、减少无效唤醒,并降低死锁/卡死风险。

4. 定制化调用通信

案例:

​ 多线程之间按顺序调用,实现AA->BB->CC。三个线程启动,要求如下:

​ AA打印5次,BB打印10次,CC打印15次

​ 接着

​ AA打印5次,BB打印10次,CC打印15次

​ 。。。打印10轮

分析实现方式:

  1. 有一个锁Lock,3把钥匙Condition
  2. 有顺序通知(切换线程),需要有标识位
  3. 判断标志位
  4. 输出线程名 + 内容
  5. 修改标识符,通知下一个

内容:

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//资源类
class Share {
    private final Lock lock = new ReentrantLock();
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();
    private final Condition condition3 = lock.newCondition();
    private Integer flag = 1;

    public void print5() throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1) {
                condition1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "  i= : " + i);
            }
            flag = 2;
            condition2.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void print10() throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                condition2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "  i= : " + i);
            }
            flag = 3;
            condition3.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void print15() throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                condition3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "  i= : " + i);
            }
            flag = 1;
            condition1.signalAll();
        } finally {
            lock.unlock();
        }
    }
}


public class DemoCondition {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(() -> {
            //都是10轮
            for (int i = 0; i < 10; i++) {
                try {
                    share.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            //都是10轮
            for (int i = 0; i < 10; i++) {
                try {
                    share.print10();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            //都是10轮
            for (int i = 0; i < 10; i++) {
                try {
                    share.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();


    }

}

在Java中,一个ReentrantLock可以与多个Condition对象一起使用,每个Condition对象可以用于不同的线程协调和通信场景,以便更精细地控制多线程之间的执行顺序和互斥访问。

为什么要多个 Condition?

因为如果只有一个 Condition,你 signal() 唤醒的可能是"错误类型"的线程,醒来发现条件不满足又得继续睡,导致大量无效唤醒、逻辑更绕、性能更差。

对比一下:

  • synchronized + wait/notify:基本只有一个隐式等待队列(this 的 monitor wait-set)
  • ReentrantLock + Condition:你可以创建多个等待队列,精细控制唤醒谁
    Condition.await() / signal()必须先 lock.lock() ,并且通常写在 try/finally 里释放锁。

await() 通常要放在 while 循环里检查条件,而不是 if,因为可能有"虚假唤醒"(spurious wakeup)。
为什么要用 final关键字 表示 lock 这个引用一旦指向某个 ReentrantLock,以后就不能再指向别的锁对象

同理 condition1/2/3 也固定绑定到这把锁创建出来的那几个 Condition。

因为 Condition 必须 和创建它的那把锁配套使用:

如果你不小心把 lock 或某个 condition 重新赋值成别的对象,代码会变得非常危险(甚至直接抛异常,比如没持有正确的锁就 await/signalIllegalMonitorStateException)。

依赖注入/锁/条件队列这种基础设施通常都应该是 final

5.小练习

**面试题:**两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信

java 复制代码
//**面试题:**两个线程,一个线程打印1-52,
// 另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信

//线程操作资源类,  资源类, 判断, 干活 ,通知


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//资源类
class MyCache {
    int flag = 0;   // 0 数字, 1 字母
    private final Lock lock = new ReentrantLock();
    private final Condition numTurn = lock.newCondition();
    private final Condition charTurn = lock.newCondition();

    public void print1To52() throws InterruptedException {
        int n = 1;
        lock.lock();
        try {
            for (int i = 0; i < 26; i++) {
                while (flag != 0) {
                    numTurn.await();
                }
                System.out.print(n++);
                System.out.print(n++);
                flag = 1;
                charTurn.signal();
            }
        } finally {
            lock.unlock();
        }
    }

    public void printAtoZ() throws InterruptedException {
        lock.lock();
        char n = 'A';
        try {
            for (int i = 0; i < 26; i++) {
                while (flag != 1) {
                    charTurn.await();
                }
                System.out.print(n++);
                flag = 0;
                numTurn.signal();
            }
        } finally {
            lock.unlock();
        }
    }

}


public class Practice {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        new Thread(() -> {

            try {
                myCache.print1To52();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();


        new Thread(() -> {
            try {
                myCache.printAtoZ();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}
相关推荐
それども2 小时前
Spring Bean 的name可以相同吗
java·后端·spring
墨雪不会编程2 小时前
C++ string 详解:STL 字符串容器的使用技巧
java·开发语言·c++
Lucky GGBond2 小时前
实践开发:老系统新增字段我是如何用枚举优雅兼容历史数据的
java
悲喜自渡7212 小时前
Python 编程(gem5 )
java·linux·开发语言
xing-xing2 小时前
JVM 内存、直接内存、系统内存、本地内存、物理内存总结
java·jvm
yangpipi-2 小时前
《C++并发编程实战》第5章 C++内存模型和原子操作
android·java·c++
qq_12498707533 小时前
基于微信小程序的电子元器件商城(源码+论文+部署+安装)
java·spring boot·spring·微信小程序·小程序·毕业设计
吃喝不愁霸王餐APP开发者3 小时前
基于Spring Cloud Gateway实现对外卖API请求的统一鉴权与流量染色
java·开发语言
a努力。4 小时前
美团Java面试被问:Redis集群模式的工作原理
java·redis·后端·面试