java多线程案例(包含解耦合介绍)

69 阅读6分钟

多线程案例

1.单例模式

- 1.1懒汉
  • 第一次使用的时候才会创建实例,所以叫懒汉

     package classTest19;
     ​
     /*
     * 懒汉的方式实现单列模式*/
     public class Test {
         private static Test  instance = null;
         /*
         * 这个引用先初始化为null,而不是立即创建实例
         *调用getInstance才会创建这个实例 */
         public static Test getInstance() {
             if (instance ==null) {
                 instance = new Test();
             }
             return instance;
         }
         
         private Test() {
             //构造方法私有化
         }
     ​
         public static void main(String[] args) {
             Test t1 = Test.getInstance();
             Test t2 = Test.getInstance();
             System.out.println(t1 == t2);
         }
     }
     ​
     ​
    
    • 代码分析
  1. 代码执行到Test t1的时候,就会创建实例,
  2. 执行到Test t2的时候,此时instance不为空,直接返回实例,
  3. 最后的结果就是为ture
  4. 同时这样的书写格式存在着一定的线程安全问题,不懂得可以参考上一篇博客思考,或者留言
  • 对线程进行加锁
 package classTest19;
 ​
 /*
 * 懒汉的方式实现单列模式*/
 public class Test {
     private  static Object cocker = new Object();
     private static Test  instance = null;
     /*
     * 这个引用先初始化为null,而不是立即创建实例
     *调用getInstance才会创建这个实例 */
 ​
     public static Test getInstance() {
         if(instance == null) {
           /*
 ​
           * 第一个if是判断是否要加锁
           第二个if判断是否要创建对象
           加两个if条件是因为 防止线程阻塞的时候
           * 别的线程修改了instance的值*/
             synchronized (cocker) {
 ​
                 if (instance == null) {
                     instance = new Test();
                 }
             }
         }
         return instance;
     }
 ​
     private Test() {
         //构造方法私有化
     }
 ​
     public static void main(String[] args) {
 ​
         Test t1 = Test.getInstance();
         Test t2 = Test.getInstance();
         System.out.println(t1 == t2);
     }
 }
 ​
  • 加锁仍然或存在一定问题 比如说堵塞等 可以使用volatile解决

  • 指令重排序:调整代码的执行顺序,保证逻辑不变的前提下,提高程序的效率

    1. new操作可能涉及到三个步骤
    2. a 申请内存
    3. b 调用构造方法
    4. c 吧内存地址复赋值给引用
  • 正常情况下是abc来执行,但是可能被优化为a c b 所以需要使用到volatile

  • volatile功能

    1. 保证内存可见性,每次访问量必须都要重新读取内存,而不会优化到寄存器中
    2. 禁止指令重排序,其修饰的变量读写相关的指令,是不能被重排序的
 package classTest19;
 ​
 /*
 * 懒汉的方式实现单列模式*/
 public class Test {
     private  static Object cocker = new Object();
     private volatile static Test  instance = null;
     /*
     比如 线程1判定instance指向一个非null的值,但是还未初始化对象 但是t2判定instance==null不成立
     直接ruturn 就会出现问题 这就是重排序引发的问题
     使用volatitle就不会出现重排序
 ​
     * 这个引用先初始化为null,而不是立即创建实例
     *调用getInstance才会创建这个实例 */
 ​
     public static Test getInstance() {
         if(instance == null) {
           /*
           * 第一个if是判断是否要加锁
           第二个if判断是否要创建对象
           加两个if条件是因为 防止线程阻塞的时候
           * 别的线程修改了instance的值*/
             synchronized (cocker) {
 ​
                 if (instance == null) {
                     instance = new Test();
                 }
             }
         }
         return instance;
     }
 ​
     private Test() {
         //构造方法私有化
     }
 ​
     public static void main(String[] args) {
 ​
         Test t1 = Test.getInstance();
         Test t2 = Test.getInstance();
         System.out.println(t1 == t2);
     }
 }
 ​

总结

  • 可能有的人会好奇懒汉设计模式有什么作用,以肯德基的疯狂星期四为例子,只有到了星期四这个模块才会出行,平时不会出现,这就为我们省去了很多不必要的开销。
- 1.2饿汉
 package classTest17;
 ​
 public class Singleton {
     /*
     * static 静态的 指的的是类的属性
     * instance 就是Singleton类对象里面持有的属性
     * 每个类的类对象只有一个,类中的static属性当然也只有一个
     * instance指向的类对象就是唯一的对象
     * 其他代码想要调用这个类的实例,只能通过这个get方法*/
     private static Singleton instance = new Singleton();
 ​
     public static Singleton getInstance() {
         return instance;
     }
     //反映出单例模式
     /*
     * 实例在类加载的过程中就创建了 程序一启动就创建了
     * 使用“饿汉” 来形容实例创建特别早,非常迫切*/
     private Singleton() {
         //其他代码无法进行new对象 避免出错
 ​
     }
 ​
 ​
 }
 ​

2.阻塞队列

  • 线程安全的

  • 满了的队列进行入队,此时就会阻塞,直到其他线程出队是才解除阻塞

  • 空了的队列出队,此时就会阻塞,直到队列不空

  • 基于阻塞队列 实现生产者消费者模型(一种多线程的方法)

  • 解耦合(降低程序之间的依赖关系)

image.png

  • 上述这个关系中,各个服务器之间依赖性就比较强,商户服务器挂了之后整个功能就不能实现了

image.png

  • 生产者往队列中写入元素 消费者在队列中消费元素 后续添加模块只需要在队列中添加即可
  • 削峰填谷

image.png

  • 即使外界的请求出现峰值,由队列来承担 防止b c因为承受不住大量的请求从事挂了
  • 生产者 --消费者模型(通过⼀个容器来解决⽣产者和消费者的强耦合问题)
 package classTest21;
 ​
 public class MyBlockintQueue {
     private String[] elems = null;
     private int head = 0;
     private  int tail = 0;
     private  int size = 0;
     //数组容量
     Object locker = new Object();
     public MyBlockintQueue(int capacity) {
         elems = new String[capacity];
     }
     //入队列
     public void  put (String elem) throws InterruptedException {
         /*如何加锁?
         * 加锁的过程中 如果不加if判断语句
         * 此时线程1进入锁中,还没有执行完毕 size的长度不变
         * 线程2等线程1解锁过后,已经跳过了判断条件
         * 此时就会导致size比原来大,可能已经超过了数组的大小*/
         synchronized (locker) {
             /*使用循环 这样使用的时候可以多进行几次判定
             * 如果是使用if 判定一次过后 进行阻塞 当线程被唤醒后不会在判断条件
             * 但是此时可能线程被唤醒过后的条件并不满足
             * 以下面为例 线程被唤醒的时候,可能此时线程还是满的
             * 但是过了判定条件,就会出现问题
             * 使用while可以避免 线程被其他线程唤醒时,可能此时的条件并不满足唤醒,由于已经过了if的判定
             * 容易出现错误 使用while就可以循环判定 避免出错 */
 ​
            while(size >= elems.length) {
                 //此时表示这个队列满了 后续需要这个代码进行阻塞
              locker.wait();//此时队列进行阻塞 当有元素出队列的时候就不在阻塞
             }
             //新的元素要放到tail指向的位置上
             elems[tail] = elem;
             tail++;
             if (tail >= elems.length) {
                 //此时又从第一个位置开始放
                 tail = 0;
             }
             size++;
             //入队列成功后进行唤醒
             locker.notify();
         }
     }
     //出队列
     public String take() throws InterruptedException {
         synchronized (locker) {
           while (size == 0) {
                 //队列为空 后续需要这个代码进行阻塞
                 //队列空了出队列,进行阻塞,直到入队列成功过后才不会阻塞
 ​
             locker.wait();
             }
             //取出head位置的元素并返回
             String elem = elems[head];
             head++;
             if (head >= elems.length) {
                 head = 0;
                 //循环 第一个位置开始
             }
             //取出过后,长度减一
             size--;
             //元素出队列过后唤醒元素
             locker.notify();
             return elem;
         }
     }
 }
 ​
 ​
 package classTest21;
 /*
 * 基于数组实现环形队列
 * 先实现普通队列
 * 加上线程安全
 * 加上阻塞功能*/
 ​
 public class Test {
     public static void main(String[] args) {
         MyBlockintQueue myBlockintQueue = new MyBlockintQueue(1000);
       /*  myBlockintQueue.put("qqq");
         myBlockintQueue.put("qddq");
         myBlockintQueue.put("qqffffq");
 ​
         String elem = " ";
         elem = myBlockintQueue.take();
         System.out.println("elem:" + elem);*/
         //生产者
         Thread t1 = new Thread(() -> {
             int n =1;
             while (true) {
                 try {
 ​
                     myBlockintQueue.put(n +"");
                     System.out.println("生产元素" + n);
                     n++;
                         Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
         //消费者
         Thread t2 = new Thread(() -> {
 ​
             while (true) {
                 try {
 ​
                      String n = myBlockintQueue.take();
                     System.out.println("消费元素" + n);
                     Thread.sleep(3000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
         t1.start();
         t2.start();
 ​
 ​
     }
 }
 ​

结果:最后的结果为当生产者生产满过后,等消费者消费了再继续生产,很好的体现了削峰填谷的思想。