JAVA多线程-生产者消费者模型

生产者消费者模型

  • 一个经典的同步模型。

  • Java中要制作这个模型,得满足以下几个条件

    • 高内聚低耦合前提下,线程操纵资源类。
    • 判断、干活、唤醒通知。
    • 严防多线程并发状态下的虚假唤醒。
  • 传统的:sync、wait、notify

  • JUC变种:lock、await、signal

  • 以上这俩种都不是我们的重点。

  • 下面还是来实现一下。

传统JUC锁版

示例
  • 双线程情况,大于双线程就得加多个 condition
class Data {

    private int number = 0; // 资源
    private Lock lock = new ReentrantLock(); // 可重入锁
    private Condition condition = lock.newCondition();

    public void increment() throws Exception{
        lock.lock();
        try {
            // 判断,防止虚假唤醒用 while 不是 if
            while (number != 0) {
                // 等待,不能生产
                condition.await();
            }

            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + "线程\t" + this.number);

            // 通知唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws Exception{
        lock.lock();
        try {
            // 判断,防止虚假唤醒用 while 不是 if
            while (number == 0) {
                // 等待,不能消费
                condition.await();
            }

            // 消费
            number--;
            System.out.println(Thread.currentThread().getName() + "线程\t" + this.number);

            // 通知唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ProducerAndConsumerTraditionalDemo {

    public static void main(String[] args) {
        Data data = new Data();
        // 要求写一个初始变量为0,两个线程交替操作,一个加一,一个减一,进行五轮,五轮后的结果得是0
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}
/*
    A线程	1
    B线程	0
    A线程	1
    B线程	0
    A线程	1
    B线程	0
    A线程	1
    B线程	0
    A线程	1
    B线程	0
*/
分析
  • 我们一定要记住用while循环来判断多线程情况下的线程阻塞
  • 这是线程 一对一 的生产者消费者模型,试想如果此时是多个生产者对多个消费者,还能保证 **number **的数据一致性吗?不能的话怎么解决呢?

阻塞队列版 (重要)

  • volatile/CAS/AtomicInteger/BlockQueue/原子引用/线程交互组合使用!
示例一
  • 线程一对一的模式,对上面的解答。
class Source {
    // 默认开启进行生产和消费
    private volatile boolean FLAG = true; // 禁止重排以及保证可见性
    // 资源
    private AtomicInteger atomicInteger = new AtomicInteger();
    // 阻塞队列
    BlockingQueue<String> blockingQueue = null;
    // 多态阻塞队列
    public Source(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        // 反射查看当前接口被什么类实现
        System.out.println(blockingQueue.getClass().getName());
    }

    public void producer() throws Exception{
        String data = null;
        while (this.FLAG) {
            data = atomicInteger.incrementAndGet() + ""; // 相当于++i
            // 两秒钟存一个
            if (blockingQueue.offer(data, 2L, TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName() + "线程\t 插入队列插入" + data + "成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + "线程\t 插入队列插入" + data + "失败!");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "线程\t 被叫停!此时FLAG = false,生产者生产结束。");
    }

    public void consumer() throws Exception{
        String result = null;
        while (this.FLAG) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if (result == null || result.equalsIgnoreCase("")) {
                this.FLAG = false;
                System.out.println(Thread.currentThread().getName() + "超过两秒没有消费成功,停止消费!");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "线程\t 消费队列消费" + result + "成功!");
        }
    }

    public void stop() {
        this.FLAG = false;
    }
}
public class ProAndConsBlockingQueueDemo {

    public static void main(String[] args) {

        Source source = new Source(new ArrayBlockingQueue<>(2));

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程\t 启动生产!");
            try {
                source.producer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "producer").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程\t 启动消费!");
            try {
                source.consumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "consumer").start();

        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        source.stop();
    }
}
/*
    producer线程	 启动生产!
    consumer线程	 启动消费!
    producer线程	 插入队列插入1成功!
    consumer线程	 消费队列消费1成功!
	...	
    producer线程	 插入队列插入20成功!
    consumer线程	 消费队列消费20成功!
    producer线程	 被叫停!此时FLAG = false,生产者生产结束。
    consumer超过两秒没有消费成功,停止消费!
*/
示例分析
  • 运行过程中,可能会出现先消费,再生产的情况!
    • 为什么会出现呢?因为我们只是使用了阻塞队列来完成某个资源的生产和消费
    • 并没有使生产和消费的线程用锁同步,所以打印语句是可能出现CPU资源抢夺而打印顺序出错
    • 但是虽然打印会出错,但是先生产和后消费的过程已经进行过了!
    • 综上程序运行过程没有出错,但是打印语句可能排序错误,因为线程与线程之间未同步!
  • 为什么要有一个叫停操作呢?
    • 因为我们在使用超时组的阻塞队列方法的过程中,可能会发生线程与线程之间的和谐生产与消费
    • 如果我们不在主线程中的某一时刻中断,那么生产和消费可能就会一直进行下去。
示例二
  • 多个消费者对多个生产者。
  • 头都给我想通了才设计出来的
/**
 * @author zhaolimin
 * @date 2021/11/16
 * @apiNote 生产者消费者模型阻塞队列实现
 */

class Source {
    // 默认的消费者线程数。
    private volatile int flag;
    // 资源
    private AtomicInteger atomicInteger = new AtomicInteger();

    // 阻塞队列
    BlockingQueue<String> blockingQueue = null;
    // 对阻塞队列的多态阻塞队列
    public Source(int flag, BlockingQueue<String> blockingQueue) {
        this.flag = flag;
        this.blockingQueue = blockingQueue;
        // 反射查看当前接口被什么类实现
        System.out.println(blockingQueue.getClass().getName());
    }

    public void producer() throws Exception{

        String data = null;

        while (this.flag > 0) {
            data = atomicInteger.incrementAndGet() + ""; // 相当于++i
            // 两秒钟存一个
            if (blockingQueue.offer(data, 2L, TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName() + "线程\t 插入队列插入" + data + "成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + "线程\t 插入队列插入" + data + "失败!阻塞队列满了!");
            }
            // 存完之后等待两秒钟
            TimeUnit.SECONDS.sleep(2);
        }
        System.out.println(Thread.currentThread().getName() + "线程\t 当前没有消费者了!停止生产!");
    }

    public void consumer() throws Exception{

        String result = null;
        while (this.flag > 0) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if (result == null || result.equalsIgnoreCase("")) {
                this.flag--;
                System.out.println(Thread.currentThread().getName() + "线程\t 超过四秒没有消费成功,销毁该线程,消费线程数减一!");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "线程\t 消费队列消费" + result + "成功!");
        }
    }

    public void stop() {
        this.flag = 0;
    }
}
public class ProAndConsBlockingQueueDemo {

    public static void main(String[] args) {

        int count = 3;
        Source source = new Source(count, new ArrayBlockingQueue<>(2));

        for (int i = 1; i <= count; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "线程\t 启动生产!");
                try {
                    source.producer();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "producer" + temp).start();
        }

        for (int i = 1; i <= count; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "线程\t 启动消费!");
                try {
                    source.consumer();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "consumer" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程叫停!");

        source.stop();
    }
}
/*
    producer1线程	 启动生产!
    producer3线程	 启动生产!
    producer2线程	 启动生产!
    consumer1线程	 启动消费!
    consumer2线程	 启动消费!
    consumer3线程	 启动消费!
    producer1线程	 插入队列插入1成功!
    producer3线程	 插入队列插入2成功!
    consumer1线程	 消费队列消费1成功!
    consumer2线程	 消费队列消费3成功!
    consumer3线程	 消费队列消费2成功!
    producer2线程	 插入队列插入3成功!
    consumer1线程	 消费队列消费4成功!
    producer1线程	 插入队列插入6成功!
    producer3线程	 插入队列插入4成功!
    consumer2线程	 消费队列消费5成功!
    consumer3线程	 超过四秒没有消费成功,销毁该线程,消费线程数减一!
    producer2线程	 插入队列插入5成功!
    consumer1线程	 消费队列消费6成功!
    producer3线程	 插入队列插入7成功!
    producer2线程	 插入队列插入8成功!
    consumer2线程	 消费队列消费7成功!
    consumer2线程	 消费队列消费9成功!
    consumer1线程	 消费队列消费8成功!
    producer1线程	 插入队列插入9成功!
    producer2线程	 插入队列插入10成功!
    producer1线程	 插入队列插入12成功!
    producer3线程	 插入队列插入11成功!
    consumer2线程	 消费队列消费10成功!
    consumer2线程	 消费队列消费12成功!
    consumer1线程	 消费队列消费11成功!
    consumer2线程	 超过四秒没有消费成功,销毁该线程,消费线程数减一!
    producer2线程	 插入队列插入13成功!
    producer3线程	 插入队列插入15成功!
    consumer1线程	 超过四秒没有消费成功,销毁该线程,消费线程数减一!
    producer2线程	 当前没有消费者了!停止生产!
    producer3线程	 当前没有消费者了!停止生产!
    producer1线程	 插入队列插入14失败!阻塞队列满了!
    producer1线程	 当前没有消费者了!停止生产!
    主线程叫停!
*/
示例分析
  • 我们可以看到只要此时的某个消费线程被销毁了,可以立即被感知到,并且flag–操作不会被指令重排,运用了 volatile 关键字的作用。
  • 同样会出现,先消费再生产的情况,具体原因见示例一的分析部分。

synchronizedlock 的区别?(重点)

原始构成

  • synchronized 是关键字,它是属于JVM层面的。
    • monitorenter:底层是通过 monitor 对象来完成,其实 wait/notify 等方法依赖 monitor 对象。
    • 所以只有在synchronized关键字同步块或者方法中才能调用 wait/notify 等方法
    • monitorexit
    • JVM指令
Code:
       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: dup
       8: astore_1
       // synchronized 关键字锁住的同步块入口
       9: monitorenter 					
      // synchronized 关键字锁住的同步块内容
      10: aload_1 					    
      // synchronized 关键字锁住的同步块出口一:表示正常退出
      11: monitorexit 					
      12: goto          20
      15: astore_2
      16: aload_1
      // synchronized 关键字锁住的同步块出口二:如果有异常我也得保证同步块被退出了
      // 保证不会产生死锁,保证
      17: monitorexit
  • lock 是具体的类,是api层面的锁。
    • JVM指令
20: new           #3                  // class java/util/concurrent/locks/ReentrantLock
23: dup
24: invokespecial #4                  // Method java/util/concurrent/locks/ReentrantLock."<init>":()V

使用方法

  • 对于 synchronized同步代码块、同步方法来说:
    • 都不需要用户去手动释放锁
    • synchronized 代码执行完后,系统会自动让线程释放对锁的占用
  • 对于 ReentrantLock 来说:
    • 需要用户去手动释放锁
    • 如果没有主动释放锁,就有可能导致出现死锁
    • 所以最好使用 lock()和unlock() 方法配合 try/finally 语句块来完成。

等待可否中断

  • **synchronized **不可中断
    • 除非抛出了异常或者正常运行完成。
  • ReentrantLock 可以中断
    • 设置超时方法 trylock(long timeout, TimeUnit unit)
    • lockInterruptibly() 放代码块中,调用 interrupt() 方法可中断。

加锁是否公平

  • synchronized 默认非公平锁。
  • ReentrantLock 可以在初始化的时候规定是否是公平锁。

锁绑定多个条件 Condition

  • synchronized 没有这个功能。
  • ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒。
    • 不会像 synchronized 那么要么随机唤醒一个线程,要么全都唤醒

案例(重要)

  • 要求:
    • 多线程之间按顺序调用,实现 A->B->C 三个线程启动。
    • A打印5次,B打印10次,C打印15次。
    • 紧接着
    • A再打印5次,B再打印10次,C再打印15次。
    • 重复 10 轮。
class shareData {
    private int threadTarget = 1; // A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    public void aPrint5() {
        lock.lock();
        try {
            // 判断线程A到底能不能继续
            while (this.threadTarget != 1) {
                conditionA.await();
            }

            // 执行
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 2;
            conditionB.signal();

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

    public void bPrint10() {
        lock.lock();
        try {
            // 判断线程B到底能不能继续
            while (this.threadTarget != 2) {
                conditionB.await();
            }

            // 执行
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 3;
            conditionC.signal();

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

    public void cPrint15() {
        lock.lock();
        try {
            // 判断线程C到底能不能继续
            while (this.threadTarget != 3) {
                conditionC.await();
            }

            // 执行
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 1;
            conditionA.signal();

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

}
public class SyncAndLockDemo {
    public static void main(String[] args) {
        shareData shareData = new shareData();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.cPrint15();
            }
        }, "C").start();
        
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.aPrint5();
            }
        }, "A").start();
        
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.bPrint10();
            }
        }, "B").start();
    }
}

/*
    A线程	1
    A线程	2
    A线程	3
    A线程	4
    A线程	5
    B线程	1
    B线程	2
    B线程	3
    B线程	4
    B线程	5
    B线程	6
    B线程	7
    B线程	8
    B线程	9
    B线程	10
    C线程	1
    C线程	2
    C线程	3
    C线程	4
    C线程	5
    C线程	6
    C线程	7
    C线程	8
    C线程	9
    C线程	10
    C线程	11
    C线程	12
    C线程	13
    C线程	14
    C线程	15
    ...
*/
案例分析
  • 虽然分析代码,我们可以不设置那么多 Condition
  • 但是我又想了以下,不是精确唤醒的话,随机唤醒,可能要唤醒好几次才能唤醒到相应的线程。
  • 这中途会浪费更多的CPU资源。
  • 所以精确唤醒某些情况下还是很有必要的。
  • 而且最重要的是,没有精确唤醒,并发状态下,可能会出现死锁情况!

死锁情况案例(重要)

package JUC.lock;

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

/**
 * @author zhaolimin
 * @date 2021/11/16
 * @apiNote 比较 sync 关键字和 lock 类的区别
 */

class shareData {
    private int threadTarget = 1; // A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    //private Condition conditionB = lock.newCondition();
    //private Condition conditionC = lock.newCondition();
    public void aPrint5() {
        lock.lock();
        try {
            // 判断线程A到底能不能继续
            while (this.threadTarget != 1) {
                conditionA.await();
            }
            // 执行
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 2;
            //conditionB.signal();
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void bPrint10() {
        lock.lock();
        try {
            // 判断线程B到底能不能继续
            while (this.threadTarget != 2) {
                //conditionB.await();
                conditionA.await();
            }
            // 执行
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 3;
            //conditionC.signal();
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cPrint15() {
        lock.lock();
        try {
            // 判断线程C到底能不能继续
            while (this.threadTarget != 3) {
                //conditionC.await();
                conditionA.await();
            }
            // 执行
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "线程\t" + i);
            }
            // 唤醒
            threadTarget = 1;
            conditionA.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class SyncAndLockDemo {
    public static void main(String[] args) {
        shareData shareData = new shareData();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.cPrint15();
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.aPrint5();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareData.bPrint10();
            }
        }, "B").start();
    }
}

/*
    A线程	1
    A线程	2
    A线程	3
    A线程	4
    A线程	5
    B线程	1
    B线程	2
    B线程	3
    B线程	4
    B线程	5
    B线程	6
    B线程	7
    B线程	8
    B线程	9
    B线程	10
    运行中。。。
*/
案例分析
  • 由于没有精确唤醒,我们走到线程C准备开始的时候,恰好被线程A的其它几次请求抢先抢占了 aprint5() 方法,导致此时一直在那里循环等待,然后线程C一直被阻塞,由于线程C被阻塞所以,不允许 cprint15() ,所以信号量改不回1,所以此时线程A的其它几趟也会卡着不动,等待资源释放,导致死锁。

码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes

如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。
										—————————————————————— 爱你们的 noblegasesgoo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值