JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式

Posted s:103

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式相关的知识,希望对你有一定的参考价值。

  • 欢迎光临 ^ V ^

文章目录

JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式

1. 定时器

  • 定时器,可以理解为闹钟
    • 我们设立一个时间,时间一到,让一个线程跑起来~
  • 而Java标准库提供了一个定时器类:
    • Timer ,from java.util

1.1 定时器Timer的使用

1.1.1 核心方法schedule

  • 传入任务引用(TimerTask task)和 “定时”(long delay / ms)

  • 由于TimerTask不是函数式接口,是普通的抽象类
    • 所以只能用匿名内部类,而不能用lambda表达式

  • 写法
public static void main(String[] args) 
    Timer timer = new Timer();
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            for (int i = 0; i < 10; i++) 
                System.out.println("好耶 ^ v ^");
            
        
    ,1000);
    System.out.println("不好耶 T . T");

  • TimerTask实现了Runnable

    • 不能传Runnable对象过去,这属于向下转型~
    • 是Runnable的一个“封装”
    • 所以,重写run方法,合情合理~
    • 只不过不能用
  • 而在Timer的schedule方法内部,则将这个线程保存起来,定时后执行~

  • 而这,有一个细节,就是执行完后,程序并没有结束,进程并没退出

原因是:

  • Timer内置了一个前台线程
    • 阻止进程退出~
    • 这并不是重点,其实就是timer在等待被安排下一个任务~

1.1.2 定时器管理多个线程

public class Test 

    public static void main(String[] args) 
        Timer timer = new Timer();
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期一好耶 ^ v ^");
            
        ,1000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期二好耶 ^ v ^");
            
        ,2000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期三好耶 ^ v ^");
            
        ,3000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期四好耶 ^ v ^");
            
        ,4000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期五好耶 ^ v ^");
            
        ,5000);
        System.out.println("今天不好耶 T . T");
    


  • 那么就安排多个任务呗~

1.1.3 定时器的使用场景

  • 应用场景特别多

    • 尤其是网络编程
  • 而这个任务等待,不应该是无期限的

    • 超时:504 【gateway timeout】
  • 定时器可以强制终止请求:浏览器内部都有一个定时器,发送请求后,定时器就开始定时;若在规定时间内,响应数据没有返回,就会强制终止请求

  • 这个方法一般在任务的run方法中调用,确定是否及时

    • 这种特殊语法不是我们能理解的,并且目前我们不需要用到这个用法~

1.2 自己实现一个定时器

  • 想法一,根据任务们的时间
    • 在添入的时候,就让他们启动并以对应的时间"睡下"
      • 有点像睡眠排序法这个消遣的笑话~
    • 显然这个方法是不科学的,线程到达一个量级,进程必然装不下
    • 系统必然卡死崩掉
  • 想法二,根据时间,到了时间自动启动~
    • 将任务们按照时间长短排序
    • 每次只看最早启动的任务就好
      • 当然,等待时间是同步的~
      • 每个任务都有在等
    • 启动,再去看接下来的任务~
      • 如果两个任务同时启动,顺序则不能确定~

是不是触动你的DNA了?

  • 没错,搞一个堆就好了
    • 每次可见堆顶元素~
    • 而小根堆堆顶正是我们这里的最早启动的任务~
    • 旧堆顶取走后,新堆顶又是剩余的最早启动的任务~
  • 而定时器的核心数据结构就是:优先级队列 ===> 堆
    • 而定时器可能被多线程使用,所以线程安全问题也要被保证
    • 队列为空,队列为“满”的时候,对操作也要有限制(不应该有无限个任务)
    • 这就需要我们的阻塞队列~

即,定时器底层就是一个阻塞优先级队列! ===> PriorityBlockingQueue

  • 对于PriorityBlockingQueue,我这里并不会去模拟~

1.2.1 属性

class MyTask 
    public Runnable runnable;
    public long time;

public class MyTimer 
    private PriorityBlockingQueue<MyTask> tasks = new PriorityBlockingQueue<>();
    

阻塞优先级队列中的元素应该有如下两个信息:

  • MyTask
  1. 执行什么任务~
  2. 任务什么时候执行~

1.2.2 建立一个MyTask对象

  1. runnable就是一个任务~
  2. time是绝对时间,而不是定时时间
    • 是”启动时间“的具体时间
    • 到达这个时间,任务才能运行~
    • 为1970.01.01那一天的00:00:00到构建对象时的此时此刻的毫秒数~
  • 获取当前时间方法:System.currentTimeMillis()
class MyTask 
    public Runnable runnable;
    public long time;
    //绝对时间戳~
    //方便判断~
    //这个不是定时时间

    public MyTask(Runnable runnable, long delay) 
        this.runnable = runnable;
        this.time = delay + System.currentTimeMillis();
    

1.2.3 schedule方法

public void schedule(Runnable runnable, long delay) 
    MyTask myTask = new MyTask(runnable, delay);
    tasks.put(myTask);

  • 构造一个myTask对象插入到队列中~

1.2.4 构造方法初步设计

public MyTimer() 
        Thread t = new Thread(() -> 
            try 
                MyTask myTask = tasks.take();
                long nowTime = System.currentTimeMillis();
                if(myTask.time <= nowTime) 
                    //启动
                else 
                    //不能启动
                
             catch (InterruptedException e) 
                e.printStackTrace();
            
        );
    

  • 定时器被构造出来后,应该就已经启动“母线程”
    • 就应该尝试【take】了
    • 只不过队列为空,要阻塞等待~
  • 之后通过schedule安排任务~【put】

  • 启动:
    • 调用run方法
  • 不能启动:
    • 将任务返回队列

1.2.5 构造方法最终设计

  • 在构造方法初步设计有两个很严重的BUG
    • 可以停止观看去想一想~
  1. 优先级对于自定义类,需要我们给“比较规则”,“优先级规则”
  2. “没有等待”以及“盲目等待”

对于1. 比较规则:

  • 只需要让MyTask实现比较接口

  • 当然也可以传比较器~(lambda表达式)

  • 两种方式都OK~

  • 左减右大于0

    • 如果代表此对象大于该对象代表升序排列 ===> 小根堆

    • 如果代表此对象小于该对象代表降序排列 ===> 大根堆

对于2. “没有等待”以及“盲目等待”

  • 上述代码只会判断一次~
    • 应该套上一个循环~

  • wait等待,唤醒起来比较方便安全

    • sleep不是一个很好的选择~
    • 因为新任务的插入,要进行唤醒
    • 超过限定时间,自动醒来
      • wait需要有锁,这里我把循环体整个框起来了
      • 我用的是“同步锁”
  • “盲目等待” 代表,这里放回去后,计算器又会判断是否可启动

    • 这样就会导致一段时间内,这个任务反复被拿来拿去无数次~
    • 相当于,上课时看表,一秒看一次,忙等
    • 而计算机,1ms就可以看很多很多次~
  • 那么我们只需要在schedule时唤醒一下,让他才判断一次就行了~

    • 这防止新插入的任务更早而被忽略
  • 大大减少判断次数!

  • 最终版:
public void schedule(Runnable runnable, long delay) 
    MyTask myTask = new MyTask(runnable, delay);
    tasks.put(myTask);
    synchronized (locker) 
        locker.notify();
    


private Object locker = new Object();

public MyTimer() 
    Thread t = new Thread(() -> 
        while(true) 
            synchronized (locker) 
                try 
                    MyTask myTask = tasks.take();
                    long nowTime = System.currentTimeMillis();
                    if(myTask.time <= nowTime) 
                        myTask.runnable.run();
                    else 
                        tasks.put(myTask);
                        locker.wait(myTask.time - nowTime);
                    
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        
    );
    t.start();

  • 别忘了启动线程~

1.3 测试MyTimer

  • 用MyTimer替换之前的Timer

  • TimeTask也可替换为Runnable,不过没关系,向上转型~

public static void main(String[] args) 
    MyTimer timer = new MyTimer();
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            System.out.println("星期一好耶 ^ v ^");
        
    ,1000);
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            System.out.println("星期二好耶 ^ v ^");
        
    ,2000);
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            System.out.println("星期三好耶 ^ v ^");
        
    ,3000);
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            System.out.println("星期四好耶 ^ v ^");
        
    ,4000);
    timer.schedule(new TimerTask() 
        @Override
        public void run() 
            System.out.println("星期五好耶 ^ v ^");
        
    ,5000);
    System.out.println("今天不好耶 T . T");

  • 测试结果正常:

  • 退出代码130,是按ctrl + f2

1.4 补充

  • 你可能也发现了,代码之中并没有完全保证,一个线程一定会在规定的时间后执行
  • 因为一个定时器,只能运行一个线程,没有并发性
    • 只是和main线程并发~
    • 所以,如果一个线程运行时间较长,会导致其后的任务“被迫延时”
    • 而判断条件不是等于等于,也有这一方面原因
      • 另一方面原因是,可能因为调度问题有误差~

  • 此时这个定时器,就只能起到,保证任务执行顺序的功能~

1.4.1 例子1

  • 例如以下测试代码:
 public static void main(String[] args) 
        MyTimer timer = new MyTimer();
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期一好耶 ^ v ^");
                try 
                    Thread.sleep(5000);
                    System.out.println("已过去五秒");
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        ,1000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期二好耶 ^ v ^");
            
        ,2000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期三好耶 ^ v ^");
            
        ,3000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期四好耶 ^ v ^");
            
        ,4000);
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("星期五好耶 ^ v ^");
            
        ,5000);
        System.out.println("今天不好耶 T . T");
    

  • 第一个任务要花5秒,而还差1秒,第二个任务就应该启动~
  • 而现象是这样的:

  • 后面的任务已经受严重延迟~

1.4.2 例子2

  • 如果一个任务死循环了,会导致后面的任务无限延期

  • 就会导致下面这种情况:

注意:

  • 这并不是我写的定时器有问题 ,Java标准库的定时器,就是这样子的, 一个定时器一个时间段里只能执行一个任务
    • 现象跟MyTimer是一样的
    • 就是这两个例子那样
  • 一个任务时间太长,会导致下一个任务延迟
  • 只起“区分先后”的作用

1.5 顺带一题

问:wait的同步锁的位置不同,结果会怎么样?

  • 例如:

  • 这两种锁的框法不同,结果一样吗?

1.5.1 后者

  • 重点就在于,没有保证take与wait是原子的~

1.5.2 前者

  • 保证原子性后:

2. 线程池

  • 跟字符串常量池和数据库连接池一样
    • 这个池的作用就提高效率,节省开销~
    • 即使线程很轻量,但是积少成多就不能忽略~
  • 只要再池子里去拿,就要比从系统申请要快~

提高效率还能提高轻量化线程“协程”,Java标准库还不支持

而线程池是一个重要的途径~

  • 从线程池里拿线程,纯纯的用户态操作
  • 而从系统上申请,就必须设计用户态和内核态之间的切换
    • 真正的创建线程,是在内核态完成的

2.1 用户态和内核态

  • 操作系统 = 内核 + 配套的应用程序
    • 内核:各种系统管理和驱动,而内核就是为了支持应用程序的
    • 这里不仅仅指核心~
      • 因为进程管理这是他的工作之一
      • 逻辑核心们也只是他的打工人~

  • 需要内核支持,才能运行的应用程序~

    • 例如,println,打印到屏幕,需要通过硬件管理~
  • 即,内核给那么多人服务,那么就不一定及时

举个栗子:

  • 去银行打印资料,前台可以帮你打印
    • 而前台在同时会去帮助其他人,给你打印好了还要好一会儿才给你~

  • 你也可以去自助打印机打印
    • 这样的时间消耗就只会缩短在 “打印需求” 内去消耗

  • 也就是说,我们在申请线程时

    • 内核态申请 ==> 内核要顾及进程管理和其他管理与驱动~

    • 用户态去拿 ==> 只需要在进程管理这个单项里去拿线程~

  • 当然,线程的诞生,还是要内核态申请

    • 放进线程池,之后在线程池里用户态拿就好~

2.2 标准库线程池类ExecutorService

  • Java标准库实现了一个接口,ExecutorService,在进程中服务线程执行~

    • 通过这个池的服务,不需要每次都申请~
  • 但是这个接口不是通过new子类对象去实例化的,而是用一个静态方法去实例化~

  • 而这里的Executors类就是“工厂类”
    • 这个类就是为了构造“线程池”而存在的
    • 这个类可以调用各种静态方法
      • 而这些静态方法使用起来简单
      • 并且可以构造各种满足我们特殊需要的对象

2.3 工厂模式

  • “工厂”

    • 即“对象工厂”,可以工厂生产出不同的对象
    • 有员工去帮你生产,使用简单
      • 降低使用成本
    • 相同原料可以有不同产品,避免参数列表相同导致无法触发重载
      • 重要作用!
  • 而工厂模式其实就是,把一个类/接口的构造方法,交给一个“工厂类”去定义

    • 即,将构造方法打包成类

Executors工厂

  • 重点掌握

你也可以自己“开个厂”

  • 就比如说,一个【堆】,泛型类是我们的自定义类
  • 而我们的自定义类要我们去规定比较方法
public class A 
    int a1;
    int a2;
    int a3;
    int a4;
    int a5;
    int a6;

  • 假设我们A类有六个成员(都是int类型)
    • 要求建立6个堆,每个堆以不同的比较规则去创建
      • 每次创建都好麻烦,都要写个比较器~
  • 只需要“开个比较器厂”,把这些构造方法包装起来就好~
    • 以后构造的时候,通过不同的方法名调用对应的构造方法~
    • 比较器Comparator
      • 构造方法基本都没有参数列表的,那么就不能用重载去解决~
        • 比较器的不同主要不是因为构造方法,而是compare被怎么重写有关~
      • compare方法重写也只能重写一个

2.3.1 开[A的构造厂]

    public static A createA1(int a) 
        //匿名内部类优先捕获全局性质变量,这里在代码块内,a1就为全局性变量~
        return new A() 
            
                this.a1 = a;
            
        

JavaEE & 线程案例 & 单例模式 and 阻塞队列


你太不小心了,被我抓住了~

你逃不了了~


文章目录

JavaEE & 线程案例 & 单例模式 and 阻塞队列

  • 单例模式是一种很经典的设计模式~

1. 设计模式

  • 下棋有棋谱

  • 弹奏有乐谱

  • 而计算机设计模式,是前人总结下来的一些代码编写套路

    • 按照这些模式,你代码写得也不会太差~
    • 兜底~
  • 主要是因为大佬的代码和其他人的代码能力差距实在是大

    • 大佬们总结了这些模式帮助我们写好代码~

设计模式有很多种(不是23种~)

2. 单例模式

2.1 单例的含义

  • 单例 ==> single instance ==> 单个实例对象

    • 也就是说,单例模式通过一些Java语法,保证某个类,只能有一个实例,即只能new一个对象~
  • 就有一些场合,限制一个类只能有一个对象,而不是多个对象去分担资源。

  • 这些限制是符合“初心写代码”,“针对性写代码”的~

  • 而单例模式有多种写法:

    1. 饿汉模式(急迫)
    2. 懒汉模式(从容)
      • 在计算机,懒是个褒义词
      • 因为可以节省开销
  • 就比如说,你打开一个1000页的pdf

    • 计算机是直接加载1000页
      • 你也没法1000页一起看,也不一定要看1000页
      • 加载1000页需要大量时间和空间
    • 还是每次只加载你看到的1-2页呢~
      • 看似只加载1-2页,你要去看其他页的时候,再给你加载~
      • 读的次数多,但是开销少~
  • 没错

    • 前者就是饿汉模式,很着急的将所有东西加载
    • 后者就是懒汉模式,非必要不加载,你给什么任务我就只做什么任务~
      • 绝对不多干一点活,非必要不做~

2.2 初步代码设计

2.2.1 饿汉模式

class Singleton 
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance() 
        return instance;
    
    private Singleton() 

    

public class Test 
    public static void main(String[] args) 
        Singleton s1 = Singleton.getInstance();
        //Singleton s2 = new Singleton();报错~
    

  • 在类加载的时候就急切地实例一个对象了
    • 这个很特殊,相当于静态代码块执行实例操作
    • 这个构造方法加载好,是可以在静态代码块内执行的~
    • 而这个单例的建立,必然是线程启动前,所以有绝对的线程安全~
  1. 特殊的语法场景,该属性是类的属性(类对象上),jvm中,类对象只有一份~
  • 那么instance就仅此一份了~

  • 一方面保证单例的特性----“初心” + “针对性”

  1. 用Java语法去禁止外部实例
  • 这个直接编译都通不过
    • 这要比抛异常方便且有效,因为抛异常在这里就有点闷声禁止
  1. 要打破单例,就必须通过“反射”
  • 反射特别不常规!是为了特定的特殊场景,【破例】去访问private
    * 比如说,玩我的世界的时候,常常有玩家说“这是我最后一次开创造”
    • 可以利用枚举类型,枚举的private属性,反射是访问不到的
      • 完美的单例
  1. 通过类名点的方式,访问这个单例
  • get方法~

2.2.2 懒汉模式

class SingletonLazy 
    private static SingletonLazy singletonLazy = null;

    public static SingletonLazy getSingletonLazy() 
        if(singletonLazy == null) 
            singletonLazy = new SingletonLazy();
        
        return singletonLazy;
    
    private SingletonLazy() 

    


public class Test 
    public static void main(String[] args) 
        SingletonLazy s1 = SingletonLazy.getSingletonLazy();
        SingletonLazy s2 = SingletonLazy.getSingletonLazy();
        System.out.println(s1 == s2);
    


  • 与饿汉不同的是
    1. 类加载的时候,并没有实例单例出来,置为初始值null
    2. 在【需要的时候】,即get方法被调用的时候
      • 判断单例是否被实例(是否为null)
      • 未被实例则立马就实例一个

2.3 线程安全角度分析

2.3.1 对于饿汉模式

  • 饿汉模式的单例,绝对是在线程启动之前,所以这一修改操作,不存在线程安全问题
  • 读操作,本身就没有线程安全问题
  • 所以目前我们认为线程是安全的
    • 自己写的时候线程不安全还是会不安全

2.3.2 对于懒汉模式

  • 对于懒汉模式而言,单例第一次实例是在第一次get的时候

    • 这个时候多条线程可能已经启动了
    • 对于单例未被实例的情况下(为null)
    • 就会进行第一次实例~
  • 而这里就会出现一个很重要的问题!

  • 回忆一波,这个场景很熟悉~

  1. 指令重排序

在这里插入图片描述

  • 指令重排序是一方面原因,可能导致,一些线程get到的单例对象,是没有执行构造方法的【毛坯房】
    • 是因为第3执行后,别的线程判定是否有单例的时候,判定为已有,直接return了~
  1. 原子性不受保证~

  • 如图,这个操作可以分为这两步~

  • 那就有以下这种极端情况~

  • 只要该线程过了“if语句这一关”,那么就会导致,多new一个对象
  1. 总结,这两种本质上,就是这一段代码是不保证“原子性”的,所以,我只需要加锁,就可以解决两个问题~

首先,先提一下单例模式的重要性

  • 在一个大工程中,一个核心的类,一个对象包含的内存数据可能是巨大的,比如100G以上~
    • 这个类只需要一个单例就行了~
    • 假设这个单例管理整个项目的加载的所有内存数据
    • 那确实一个就够了
  • 但是,由于线程不安全,即使是低概率事件而引发多new一次(100G -> 200G)
    • 那就是个大事故了~
    • 并且可能再极端一点,new了3个4个的…

2.4 处理懒汉模式线程不安全问题

  1. 法1:

  1. 法2:

  • 这个是错误的!
  • 这只是解决指令重排序,但是并没有完全解决问题~
    • 仍然有可能会出现两个线程同时过了“if大关”~

  • 这个才是正确的~
  1. 法3:
  • 法1法2都有个弊端
    • 就是加锁太频繁了
    • 加锁这个操作本身就开销大,因为其他线程就得阻塞
    • 而实际情况是,没必要多次锁,只需要锁第一次,以后就不会有事~
  • 所以可以这么搞:

  • 两个if是形式一样的,但是目的是不一样的~
    • 内层if是为了防止多次new
    • 外层if是为了尽量减少加锁的次数
      • 当然,可能有极端情况,锁了两次三次的,但是无伤大雅
      • 因为外层if,依旧不保证原子性~
  • 这样设计,在锁过一次之后,基本情况上,就不会再锁了~
  • 但是这种写法,却又有一个缺陷
    • 就是指令重排序的坑,有被挖出来了
    • 法1法2保证了完全的原子性
    • 但是法3没有,因为外层if的存在,是不原子的~
    • 那就会有以下情况:
      • 因为该情况下,进入if语句是不需要争夺锁的~
      • 所以锁在这里并没有解决指令重排序的后果

  • 所以在这里还要对singletonLazy进行禁止指令重排序操作
  • 即使用volatile~

  • 这样,法3就是较优且解决线程安全问题的方式了~

3. 阻塞队列

  • 队列—>先进先出,排好队~
  • 优先级队列—> PriorityQueue —> 堆
  • 阻塞队列—> 带有阻塞特性

3.1 阻塞特性

  1. 如果队列为空
    • 尝试出队列,就要阻塞等待,直到队列不为空
  2. 如果队列为满
    • 尝试入队列,就要阻塞等待,直到队列不为满
  3. 是线程安全的

3.2 Java标准库内自带的阻塞队列BlockingQueue接口

  • BlockingDeque代表的是双端的队列
    • 对应的就是LinkedBlockingDeque和ArrayBlockingDeque

  • 链表实现,默认最大容量是int的极限最大值

  • 顺序表实现,没有给默认容量,必须自己定~
    • 这很合理,因为如果默认为int极限最大值
    • 一下子创建那么大容量的数组,显然是不合理的

3.2.1 方法1 put入队列

  • 多线程编程特别常见且常有的异常

  • 运行结果是这样的:(ctrl + f2终止程序)

3.2.2 方法2 take出队列

  • 现在队列里有五个元素,我take六次
  • 执行结果是这样的:

  • 确实按照先入先出,但是程序好像并没有结束,这是因为第六次take的时候,发现是空队列,阻塞等待了~
    • 五次则刚好可以结束~
    • 可按ctrl + f2结束程序~


这两个方法是阻塞队列的核心方法,其他方法跟普通队列别无二致~

peek的时候队列空,阻塞等待,但是几乎不用这个方法和其他方法~

3.3 阻塞队列的好处

  • 写多线程代码时,多线程之间若进行数据交互,可以用阻塞队列简化代码编写~

  • 在go语言中,支持多并发编程,并且引入了一个“轻量级线程”的“协程”,协程与协程之间进行交互数据的时候,会通过一个阻塞队列:channel

3.3.1 生产者消费者模型

  • 这是很关键的,服务器开发中一种很常见的代码写法~
  • 我们更希望代码执行起来更像“流水线”一样

举个栗子:

  • 这个包饺子工程,显然方案二的效率会更高
    • 两个线程同时进行,进行数据交互
  • 这个模型就是“生产者消费者模型”

3.3.2 生产者消费者模型的优点

  1. 解耦合
  • 我们常听到一个词语“高内聚低耦合”
    • 这是代码风格的良好习惯~
    • 耦合代表,两个模块关联度越高,耦合性越高,关联度越低,耦合性越低
    • 内聚代表,关联度高的模块应该聚集在一起,则为高内聚,反之,低内聚~

  • 在这里,以阻塞队列为中介,即作为两个线程进行数据交互的桥梁
  • 这样就可以减少生产者与消费者之间的关联度,即解耦合
    • 这样子做有利于防止一个线程bug严重影响另一个线程
  • 生产者只认识队列不知道消费者存在
  • 消费者只认识队列不知道生产者存在
    • 一方挂了对另一方影响较小
    • 并且,引入一个新生产者,和新消费者,都很好办
      • 只需要新人与阻塞队列联系就好了

      • 新人的到来也对其他人影响最小化了

由于阻塞队列非常好使,大佬们将阻塞队列功能单独拎出来做成一个单独的服务器~

  • 消息队列服务器~
  • 这个服务器我们以后可能会用到,核心数据结构就是阻塞队列~
  • 这个服务器会挂吗
    • 会,但是其概率比你写的代码挂的概率低得低得低~
    • 人家可是固定下来的,大佬写的~
  1. 削峰填谷

  • 如图,该曲线可以代表,擀面皮产生的面皮量与时间的关系
  • 而图中出现的波峰与波谷就会产生一些问题
    • 如果没有阻塞队列
      • 波峰的产生会导致消费者一下子接受大量的数据,而常常消费者的“条件”是低于生产者的,所以消费者很可能会遇到麻烦~
      • 波谷的产生会导致消费者很快的消耗完饺子皮,那么就会处于无饺子皮的情景~
    • 而削峰填谷的含义就是 “中和”,让线程之间的交互更加稳定

  1. 生产者生产太多,导致队列满了,则进入阻塞,直到队列不满
  2. 消费者消耗太多,导致队列空了,则进入阻塞,直到队列不空
  • 有点像三峡大坝,上流水太多关闸门防洪,下流水太少开闸门防旱

3.4 代码实现生产者消费者模型

  • 下面将介绍一些情景~

    • 大部分是线程不安全的~

    • 当这只是打印顺序问题~

    • 但是阻塞队列绝对是安全的

    • 还有“死锁”情况

3.4.1 生产者 < 消费者

public class Test1 

    public static void main(String[] args) 
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);

        Thread t1 = new Thread(() -> 
            for (int i = 0; i < 25; i++) 
                try 
                    Thread.sleep(100);
                    blockingQueue.put(1);
                    System.out.println("生产1个");
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t1.start();
        Thread t2 = new Thread(() -> 
            for (int i = 0; i < 25; i++) 
                try 
                    Thread.sleep(10);
                    blockingQueue.take();
                    System.out.println("消耗1个");
                 catch (InterruptedException e) 
                    e.printStackTrace();
                

            
        );
        t2.start();
    
    

  • 我们理论上想要的结果是,生产一个消费一个,因为是消费者在阻塞(速度快)
  • 而结果是:
    • 这里是因为,消费者阻塞被唤醒时,生产者线程还来不及打印那句话~

    • 只需要加一把锁就行了~

      • 至于锁在哪个线程加,无所谓~

  1. 由于线程2比较快,所以它的take基本上是在等put的
    • 由于take和println非原子,所以有以上这种情况
  2. 即使线程2,但是还是有可能put被调度在take前
    • 只是缺少了个线程阻塞的过程罢了,本质上就是线程有元素了,不需要阻塞
    • 皆大欢喜~
    • 但是也因为非原子性,会有以上这种情况~
  • 修改线程2:(快捷键,鼠标选中需包围的语句 + ctrl + alt + t + synchronized)
Thread t2 = new Thread(() -> 
    for (int i = 0; i < 25; i++) 
        try 
            Thread.sleep(10);

            synchronized (blockingQueue) 
                blockingQueue.take();
                System.out.println("消耗1个");
            
         catch (InterruptedException e) 
            e.printStackTrace();
        

    
);
t2.start();

  • 结果:

  • 不要加两把,会死锁!
    • 因为加两把
    • 生产者要填入元素,也要进行阻塞等待(等待锁)
    • 那么就无法唤醒消费者,导致两人都进入阻塞态~

改动两个线程:

public static void main(String[] args) 
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);
    Thread t1 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(100);
                synchronized (blockingQueue) 
                    blockingQueue.put(1);
                    System.out.println("生产1个");
                
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    );
    t1.start();
    Thread t2 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(10);
                synchronized (blockingQueue) 
                    blockingQueue.take();
                    System.out.println("消耗1个");
                
             catch (InterruptedException e) 
                e.printStackTrace();
            

        
    );
    t2.start();

  • 死锁了:

3.4.2 生产者 > 消费者

public static void main(String[] args) 
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);
    Thread t1 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(10);
                blockingQueue.put(1);
                System.out.println("生产1个"); 
            catch (InterruptedException e) 
                e.printStackTrace();
            
        
    );
    t1.start();
    Thread t2 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(100);
                blockingQueue.take();
                System.out.println("消耗1个");
             catch (InterruptedException e) 
                e.printStackTrace();
            

        
    );
    t2.start();

  • 同样的,一个锁都不加的情况下,也会出现一些差错~

  • 加一把锁后,结果正常~
    • 双锁会死锁~
public static void main(String[] args) 
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);
    Thread t1 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(10);
                blockingQueue.put(1);
                System.out.println("生产1个"); 
            catch (InterruptedException e) 
                e.printStackTrace();
            
        
    );
    t1.start();
    Thread t2 = new Thread(() -> 
        for (int i = 0; i < 25; i++) 
            try 
                Thread.sleep(100);

                synchronized (blockingQueue) 
                    blockingQueue.take();
                    System.out.println("消耗1个");
                

             catch (InterruptedException e) 
                e.printStackTrace();
            

        
    );
    t2.start();

3.4.3 正常写法

  • 一般不会让两个线程都不sleep

    • 这样,“打印”这个操作就很大概率会结果出错
  • 让一方留足够的时间等对方~


    public static void main(String[] args) 
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);
        Object o = new Object();
        Thread t1 = new Thread(() -> 
            for (int i = 0; i < 25; i++) 
                try 
                    System.out.printlnJavaEE基础

Java Review - 线程池资源一直不被释放案例&源码分析

Java Review - 线程池资源一直不被释放案例&源码分析

Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析

Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析

多线程安全问题产生&解决方案

(c)2006-2024 SYSTEM All Rights Reserved IT常识