JavaSE-经典多线程样例

十念  论坛元老 | 2024-12-5 20:55:25 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1034|帖子 1034|积分 3102

单例模式

计划模式开端引入

啥是计划模式?


  • 计划模式好⽐象棋中的 “棋谱”. 红⽅当头炮, ⿊⽅⻢来跳. 针对红⽅的⼀些⾛法, ⿊⽅应招的时候有⼀些固定的套路. 按照套路来⾛局面就不会吃亏.软件开发中也有很多常⻅的 “题目场景”. 针对这些题目场景, ⼤佬们总结出了⼀些固定的套路. 按照这个套路来实现代码, 也不会吃亏, 不针对某一种语言, 而是针对某种开发场景
  • 计划模式并不是只有23种, 因为之前有些大佬写了一本书叫计划模式,重点讨论了23种, 但事实上存在更多种的计划模式
  • 计划模式与框架的区别就是, 计划模式在开发中是软性要求(不一定遵守), 但是框架是硬性要求(一定要遵守)
简单点一句话总结
计划模式是前人根据一些开发场景给出的一些经验之谈, 以是计划模式并不针对某一种语言
为何存在单例模式



  • 单例模式能保证某个类在程序中只存在唯⼀⼀份实例, ⽽不会创建出多个实例.
    这⼀点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要⼀个,再
    比如如果一个类的创建需要加载的数据量非常的巨大(GB级别), 那我们不渴望这
    个类频繁的创建销毁(开销很大), 我们大概只是渴望创建一次就可以了
饿汉式单例模式

顾名思义, 这种方式实现的单例模式非常"饥渴", 不管利用不利用都会提前new一个对象
流程如下


  • 构造方法私有化
  • 界说一个静态的类对象用以返回
  • 提供一个公开的静态接口来获取唯一的对象
测试代码如下
  1. /**
  2. * 下面定义一个类来测试饿汉式单例模式
  3. */
  4. class HungrySingleton{
  5.     // 提供一个静态的变量用来返回
  6.     private static HungrySingleton hungrySingleton = new HungrySingleton();
  7.     // 构造方法私有化(在外部不可以构造对象)
  8.     private HungrySingleton(){
  9.     }
  10.     // 提供一个获取实例的静态公开接口
  11.     public static HungrySingleton getInstance(){
  12.         return hungrySingleton;
  13.     }
  14. }
  15. public class DesignPatternTest {
  16.     public static void main(String[] args) {
  17.         // 对饿汉式单例的测试
  18.         HungrySingleton instance1 = HungrySingleton.getInstance();
  19.         HungrySingleton instance2 = HungrySingleton.getInstance();
  20.         // 测试两者是不是一个对象
  21.         System.out.println(instance1 == instance2);
  22.     }
  23. }
复制代码
测试结果

很显着, 用这种方式创建的实例都是只有一份的…
饿汉式缺陷以及是否线程安全

首先饿汉式的单例模式缺陷优劣常显着的


  • 饿汉式不管我们利用这个对象与否, 都会在类加载的时期(因为是静态对象)构建一个这样的对象, 但我们想要达成的结果是, 在我们不需要这种类的实例的时候, 我们不去进行构造对象的操作(变自动为被动)来淘汰内存等相关资源的开销
但是饿汉式单例一定是线程安全的


  • 构建对象的时期是类加载的时候, 后期差别线程对于这个实例的操作也仅仅是涉及到读操作, 不涉及修改操作, 以是固然是线程安全的, 不存在线程安全题目, 但是另一种实现的模式就不一定了
懒汉式单例模式

上面说了饿汉式单例模式的缺陷, 我们尝试利用懒汉式单例的方式去解决这个题目, 也就是仅仅在需要的时候进行new对象的操作
最基础的懒汉单例模式
构造的逻辑


  • 构造方法私有化
  • 提供一个静态的对象用来返回(临时不new对象)
  • 提供一个公开访问的静态接口来返回唯一的对象
代码测试(最基础的版本)
  1. /**
  2. * 下面定义一个类来测试懒汉式单例模式
  3. */
  4. class LazySingleton{
  5.     // 提供一个静态的变量用来返回
  6.     private static LazySingleton lazySingleton = null;
  7.     // 构造方法私有化(不可以在外部new对象)
  8.     private LazySingleton(){
  9.     }
  10.     // 提供一个公开的获取实例的接口
  11.     public static LazySingleton getInstance(){
  12.         if(lazySingleton == null){
  13.             lazySingleton = new LazySingleton();
  14.         }
  15.         return lazySingleton;
  16.     }
  17. }
  18. public class DesignPatternTest {
  19.     public static void main(String[] args) {
  20.         // 对懒汉式单例的测试
  21.         LazySingleton instance1 = LazySingleton.getInstance();
  22.         LazySingleton instance2 = LazySingleton.getInstance();
  23.         // 测试两者是不是一个对象
  24.         System.out.println(instance1 == instance2);
  25.     }
  26. }
复制代码
基础懒汉式缺陷以及是否线程安全

这个就和上面饿汉有较大的区别了, 虽然解决了在需要的时候进行new对象, 上面的基础版本的懒汉式在单线程的情况下肯定是没题目标, 但是在多线程的情况下就欠好说了…看下面的分析
如果在多线程的情况下(我们假设有t1, t2)是下图的实行顺序

很显着这是一种类似串行的实行策略
但是还大概是下图的情况

t1线程判断完毕之后没有来得及进行new对象, t2线程紧接着进行了一次完备的new对象的过程, 此时t1线程又进行了一次new对象的过程, 很显着, 我们上面的情况进行了两次构造对象的过程, 同时拿到的对象也不一致
我们通过Thread.sleep()的方式进行延迟观察看是否会发生
  1. /**
  2. * 下面定义一个类来测试懒汉式单例模式
  3. */
  4. class LazySingleton{
  5.     // 提供一个静态的变量用来返回
  6.     private static LazySingleton lazySingleton = null;
  7.     // 构造方法私有化(不可以在外部new对象)
  8.     private LazySingleton(){
  9.     }
  10.     // 提供一个公开的获取实例的接口
  11.     public static LazySingleton getInstance() throws InterruptedException {
  12.         if(lazySingleton == null){
  13.             Thread.sleep(1000);
  14.             lazySingleton = new LazySingleton();
  15.         }
  16.         return lazySingleton;
  17.     }
  18. }
  19. public class DesignPatternTest {
  20.     private static LazySingleton instance1 = null;
  21.     private static LazySingleton instance2 = null;
  22.     public static void main(String[] args) throws InterruptedException {
  23.         // 创建两个线程获取实例
  24.         Thread t1 = new Thread(() -> {
  25.             try {
  26.                 instance1 = LazySingleton.getInstance();
  27.             } catch (InterruptedException e) {
  28.                 e.printStackTrace();
  29.             }
  30.         });
  31.         Thread t2 = new Thread(() -> {
  32.             try {
  33.                 instance2 = LazySingleton.getInstance();
  34.             } catch (InterruptedException e) {
  35.                 e.printStackTrace();
  36.             }
  37.         });
  38.         // 开启两个线程
  39.         t1.start();
  40.         t2.start();
  41.         // 睡眠等待一下
  42.         Thread.sleep(2000);
  43.         System.out.println(instance1 == instance2);
  44.     }
  45. }
复制代码

很显着, 这样的懒汉式的代码是线程不安全的, 那要如何进行改进呢???
懒汉式单例模式的改进

之前我们说了, 要想保证线程是安全的, 有几种解决方式, 这里面我们就接纳加锁, 因为其实
判断是不是null和对象应该是一个整体的原子性的操作
改进之后的代码
  1. /**
  2. * 下面定义一个类来测试懒汉式单例模式(改进版)
  3. */
  4. class LazySingleton{
  5.     // 提供一个静态的变量用来返回
  6.     private static LazySingleton lazySingleton = null;
  7.     // 构造方法私有化(不可以在外部new对象)
  8.     private LazySingleton(){
  9.     }
  10.     // 提供一个公开的获取实例的接口
  11.     public static LazySingleton getInstance() {
  12.         // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
  13.         synchronized (LazySingleton.class){
  14.             if(lazySingleton == null){
  15.                 lazySingleton = new LazySingleton();
  16.             }
  17.         }
  18.         return lazySingleton;
  19.     }
  20. }
  21. public class DesignPatternTest {
  22.     private static LazySingleton instance1 = null;
  23.     private static LazySingleton instance2 = null;
  24.     public static void main(String[] args) {
  25.         // 在多线程中获取实例
  26.         Thread t1 = new Thread(() -> {
  27.             instance1 = LazySingleton.getInstance();
  28.         });
  29.         Thread t2 = new Thread(() -> {
  30.             instance2 = LazySingleton.getInstance();
  31.         });
  32.         System.out.println(instance1 == instance2);
  33.     }
  34. }
复制代码
这时候肯定是一个线程安全的代码了, 但是思考可不可以进一步改进呢???

当我们已经new个一次对象之后, 如果后续的线程想要获取这个对象, 那就仅仅是一个读操作了, 根本不涉及对对象的修改, 但是我们每次都利用锁这样的机制就会造成阻塞, 也就会导致程序的效率降落, 以是我们对代码进行了下面的修改在外层再加一个if判断
改进的方法如下
  1. // 提供一个公开的获取实例的接口
  2.     public static LazySingleton getInstance() {
  3.         // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
  4.         if (lazySingleton == null) {
  5.             synchronized (LazySingleton.class) {
  6.                 if (lazySingleton == null) {
  7.                     lazySingleton = new LazySingleton();
  8.                 }
  9.             }
  10.         }
  11.         return lazySingleton;
  12.     }
复制代码
我们的两个if的含义


  • 第一个if: 判断对象是否创建完毕, 如果创建了, 只是一个读操作
  • 第二个if: 判断是不是需要new对象
大概初学多线程的时候, 看上述代码以为很迷惑, 但其实这是因为之前我们写的程序都是单线程的情况, 单线程中实行流只有一个, 两次相同的if判断其实是没有必要的, 但是多线程的条件下, 是多个实行流, 相同的逻辑判断条件也大概产生差别的结果
完备代码(变量volatile)

关于变量是否会产生指令重排序和内存可见性题目, 我们直接加上volatile即可
  1. /** * 下面界说一个类来测试懒汉式单例模式(完备改进版) */class LazySingleton {    // 提供一个静态的变量用来返回    private volatile static LazySingleton lazySingleton = null;    // 构造方法私有化(不可以在外部new对象)    private LazySingleton() {    }    // 提供一个公开的获取实例的接口
  2.     public static LazySingleton getInstance() {
  3.         // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
  4.         if (lazySingleton == null) {
  5.             synchronized (LazySingleton.class) {
  6.                 if (lazySingleton == null) {
  7.                     lazySingleton = new LazySingleton();
  8.                 }
  9.             }
  10.         }
  11.         return lazySingleton;
  12.     }
  13. }
复制代码
阻塞队列

生产者消费者模型

关于生产者消费者模型, 其实是生活中抽象出来的一个模型案例, 我们举一个包饺子的例子来简单解释一下


  • 在包饺子的过程中, 存在一个擀饺子皮的人, 我们称之为生产者, 擀出来的饺子皮放到一个竹盘上, 这个竹盘相当于一个中央的媒介, 生产者生产的物质在上面与消费者进行交互, 而包饺子的人就是一个消费者, 从中央媒介中取出东西, 也就是消费的过程, 我们的中央的竹盘相当于一个缓冲, 如果包饺子的人包的快的话, 就需要等候做饺子皮的人, 如果做饺子皮的人做的快的话, 当竹盘放不下的时候就需要阻塞等候
  • 上面的景象抽象成生产者消费者模型, 擀饺子皮的人是生产者, 竹盖是阻塞队列, 包饺子的人是消费者
生产者消费者模型的案例以及优点

请求与响应案例

生产者消费者模型我们举一个"请求响应的案例"

图中我们也有解释, 越靠上游的斲丧的资源越少
假设我们现在出现一个秒杀的请求, 上游大概还可以运行, 但是下游的服务器由于并发量过大就直接崩溃了

以是我们一样平常会对上面提供服务的逻辑进行改变
添加一个中央的结构(阻塞队列, 大概说消息队列)进行缓冲


在真实的开发场景当中, 阻塞队列甚至会单独的摆设为一台服务器, 这种独立的服务器结构叫做消息队列, 可见其重要性

解耦合

生产者消费者模型的一个重要的优点就是让消费者和生产者解耦合


  • 根据上面的模型分析, 不管是生产者还是消费者都是面向阻塞队列来进行任务的实行的, 以是就降低了两者之间的耦合度, 未来想要修改这个模型的工作内容, 也只需要面向阻塞队列操作更改(其实相当于接口), 如果没有这种机制的话, 我们想要更改一个操作逻辑, 就需要同时修改消费者与生产者的代码结构…, 我们先前学习的接口其实就是一种解耦合的策略, 其焦点就是淘汰耦合度, 便于对代码结构进行调整
削峰填谷

刚才我们的谁人模型就说了, 如果消息请求量非常大的时候, 如果没有消息队列的存在, 就会对下游的服务器产生较大的影响, 甚至会导致服务器崩溃

下图是正常情况下消息队列的工作表示图, 添加的任务加入消息队列, 然后下游的服务器以一个相对稳固的效率从队列中取出来任务进行处理


下图是当任务量激增的时候, 虽然任务量激增, 但是依旧进入消息队列进行等候处理, 此时下游的服务器对任务的处理的效率基本稳固, 以是可以保证处理的稳固性, 不至于让下游服务器崩溃, 因为这个消息一样平常都是一阵一阵的激增, 以是比及下一轮消息量淘汰的时候, 对先前消息队列的数据进行清理即可…

阻塞队列的内置API

下图是我们相关的阻塞队列的内置API继承逻辑


关于构造方法
ArrayBlockingQueue: 必须指定大小
LinkedBlockQueue: 可以指定也可以不指定

关于offer和poll与put和take的区别
首先是offer和poll

这两个方法也可以使阻塞队列产生阻塞的结果, 但是我们可以指定一个最大的等候时间
我们利用下面的代码测试
  1. /**
  2. * 关于阻塞队列的相关测试
  3. */
  4. public class ThreadTest {
  5.     public static void main(String[] args) {
  6.         // 生成一个阻塞队列(指定队列的大小为100)
  7.         BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(100);
  8.         // 创建两个线程测试
  9.         Thread producer = new Thread(() -> {
  10.             for(int i = 0; i < 1000; i++){
  11.                 try {
  12.                     blockingQueue.offer(i, 10L, TimeUnit.SECONDS);
  13.                     System.out.println("生产了元素: " + i);
  14.                 } catch (InterruptedException e) {
  15.                     e.printStackTrace();
  16.                 }
  17.             }
  18.         });
  19.         // 消费者线程
  20.         Thread consumer = new Thread(() -> {
  21.             while(true){
  22.                 try {
  23.                     // 进行休眠
  24.                     Thread.sleep(1000 * 1);
  25.                     int elem = blockingQueue.take();
  26.                     System.out.println("消费了元素: " + elem);
  27.                 } catch (InterruptedException e) {
  28.                     e.printStackTrace();
  29.                 }
  30.             }
  31.         });
  32.         producer.start();
  33.         consumer.start();
  34.     }
  35. }
复制代码
分析下这个程序的实行的逻辑


  • 在程序启动的很短的时间内, 由于阻塞队列的容量另有空余, 以是会大量的生产元素直到阻塞队列满了, 因为消费者线程是每一秒钟斲丧一个元素, 以是存在等候时间, 我们上述代码设置的最大的等候时间是10s, 以是根本来不及等候到最大的时间点就可以进行取出元素…

put和take方法


  • 这组方法和上组方法的区别就是, 这个方法是当队列满大概队列空, 我们进行无穷期的阻塞…, 直到队列中的元素不为空大概不为满就可以进行操作
  1. /**
  2. * 关于阻塞队列的相关测试
  3. */
  4. public class ThreadTest {
  5.     public static void main(String[] args) {
  6.         // 生成一个阻塞队列(指定队列的大小为100)
  7.         BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(100);
  8.         // 创建两个线程测试
  9.         Thread producer = new Thread(() -> {
  10.             for(int i = 0; i < 1000; i++){
  11.                 try {
  12.                     blockingQueue.put(i);
  13.                     System.out.println("生产了元素: " + i);
  14.                 } catch (InterruptedException e) {
  15.                     e.printStackTrace();
  16.                 }
  17.             }
  18.         });
  19.         // 消费者线程
  20.         Thread consumer = new Thread(() -> {
  21.             while(true){
  22.                 try {
  23.                     // 进行休眠
  24.                     Thread.sleep(1000 * 1);
  25.                     int elem = blockingQueue.take();
  26.                     System.out.println("消费了元素: " + elem);
  27.                 } catch (InterruptedException e) {
  28.                     e.printStackTrace();
  29.                 }
  30.             }
  31.         });
  32.         producer.start();
  33.         consumer.start();
  34.     }
  35. }
复制代码
末了的实行结果如下

在短时间之内进行大量的生产之后开始隔一秒拿出一个元素, 生产一个元素
阻塞队列的模拟实现

关于wait和while的搭配利用


上面是我们的JDK资助文档对wait利用的建议(其实就是源码), 我们官方文档中提倡wait的利用建媾和while循环搭配, 而不是和if搭配…原因下面解释
模拟实现

其实就是一个循环队列, 在put方法加入元素的时候如果队列是满的就进行阻塞, 在take方法拿出元素的时候如果队列是空的也进行阻塞(利用wait), 然后put方法添加了一个元素之后, 利用notify方法对take正在阻塞的线程进行唤醒(随机唤醒), 下面是实现代码
  1. /**
  2. * 自己实现一个阻塞队列
  3. * 1. 使用循环数组
  4. * 2. 使用wait-notify进行线程见的通信
  5. * 3. 关于wait的使用的while机制
  6. */
  7. public class MyBlockingQueue {
  8.     // 我们定义这个阻塞队列中的元素是int类型
  9.     private int capacity = 0;
  10.     private int[] queue = null;
  11.     // 构造方法
  12.     public MyBlockingQueue(int capacity) {
  13.         this.capacity = capacity;
  14.         queue = new int[capacity];
  15.     }
  16.     // 定义队首尾的指针以及元素个数
  17.     private int first = 0;
  18.     private int last = 0;
  19.     private int size = 0;
  20.     // 判断队列是否为空
  21.     private boolean isEmpty() {
  22.         return size == 0;
  23.     }
  24.     // 判断队列是否是满的
  25.     private boolean isFull() {
  26.         return size == capacity;
  27.     }
  28.     // put操作
  29.     public void put(int val) throws InterruptedException {
  30.         while (isFull()) {
  31.             // 此时进入阻塞等待
  32.             synchronized (this) {
  33.                 this.wait();
  34.             }
  35.         }
  36.         queue[last] = val;
  37.         last = (last + 1) % capacity;
  38.         size++;
  39.         // 随机唤醒一个线程
  40.         synchronized (this) {
  41.             this.notify();
  42.         }
  43.     }
  44.     // take操作
  45.     public int take() throws InterruptedException {
  46.         while (isEmpty()) {
  47.             // 此时进入阻塞等待
  48.             synchronized (this) {
  49.                 this.wait();
  50.             }
  51.         }
  52.         int res = queue[first];
  53.         first = (first + 1) % capacity;
  54.         size--;
  55.         // 随机唤醒一个线程
  56.         synchronized (this) {
  57.             this.notify();
  58.         }
  59.         return res;
  60.     }
  61. }
  62. class Test {
  63.     public static void main(String[] args) {
  64.         // 对实现的队列进行测试
  65.         MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);
  66.         // 创建生产者线程进行测试
  67.         Thread producer = new Thread(() -> {
  68.             for(int i = 0; i < 1000; i++){
  69.                 try {
  70.                     myBlockingQueue.put(i);
  71.                     System.out.println("生产了元素: " + i);
  72.                 } catch (InterruptedException e) {
  73.                     e.printStackTrace();
  74.                 }
  75.             }
  76.         });
  77.         // 创建消费者线程进行测试
  78.         Thread consumer = new Thread(() -> {
  79.             for(int i = 0; i < 1000; i++){
  80.                 try {
  81.                     Thread.sleep(1000);
  82.                     int getElem = myBlockingQueue.take();
  83.                     System.out.println("消费了元素: " + getElem);
  84.                 } catch (InterruptedException e) {
  85.                     e.printStackTrace();
  86.                 }
  87.             }
  88.         });
  89.         // 启动两个线程
  90.         producer.start();
  91.         consumer.start();
  92.     }
  93. }
复制代码

刹时产出100个元素之后进行阻塞, 产出一个斲丧一个…

为什么要利用while代替if
  1. // put操作
  2.     public void put(int val) throws InterruptedException {
  3.         if(isFull()) {
  4.             // 此时进入阻塞等待
  5.             synchronized (this) {
  6.                 this.wait();
  7.             }
  8.         }
  9.         queue[last] = val;
  10.         last = (last + 1) % capacity;
  11.         size++;
  12.         // 随机唤醒一个线程
  13.         synchronized (this) {
  14.             this.notify();
  15.         }
  16.     }
  17. // put操作
  18.     public void put(int val) throws InterruptedException {
  19.         while (isFull()) {
  20.             // 此时进入阻塞等待
  21.             synchronized (this) {
  22.                 this.wait();
  23.             }
  24.         }
  25.         queue[last] = val;
  26.         last = (last + 1) % capacity;
  27.         size++;
  28.         // 随机唤醒一个线程
  29.         synchronized (this) {
  30.             this.notify();
  31.         }
  32.     }
复制代码
我们分析一下两个相同的操作, 利用while和if的区别

这一张图片展现了为什么利用wait搭配while利用更加公道

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

十念

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表