多线程--单例模式and工厂模式

一.什么是设计模式

设计模式好⽐象棋中的"棋谱".红⽅当头炮, ⿊⽅⻢来跳.针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀ 些固定的套路.按照套路来⾛局势就不会吃亏.

软件开发中也有很多常⻅的"问题场景".针对这些问题场景,⼤佬们总结出了⼀些固定的套路.按照这 个套路来实现代码,也不会吃亏.

设计模式与框架(spring...)的区别

框架是一种硬性要求 就是必须服从框架的规范

设计模式是软性要求 灵活度高 根据需求可以调整

二. 什么是单例模式

单例模式是主流23种设计模式的一种具体是哪23种可以AI一下

单例模式是一种典型的模式,也是比较简单的模式,日常开发中更容易用到的模式

单例模式能保证某个类在程序中只存在唯⼀⼀份实例,不能创建多个实例

比如在JDBC中的DateSource就只能创建一个实例

毕竟数据库的数据大小动辄上百的G 多创建一个实例多的存储开销可不是一般的大,更影响性能,数据库的数据就那些创建多个实例也没必要

单例模式具体的实现⽅式有很多,常见的还是饿汉模式和懒汉模式

饿汉模式:

复制代码
class SingletonHungry {                                                                                         
    //静态成员的初始化是在加载的阶段触发的 类加载往往就是在程序时候就会触发                                                                       
    private static SingletonHungry instance = new SingletonHungry();//如果带参数 下面构造方法 也要调用带有参数的                    
                                                                                                                
    public static SingletonHungry getInstance() {                                                               
        return instance;//统一后面用这个方法来获取实例                                                                        
                 //由于这里是读取操作不涉及线程安全                                                                             
    }

//.........类的内容                                                                                                           
                                                                                                                
    //私人构造方法                                                                                                    
    private SingletonHungry() {                                                                                 
        //点睛之笔 不让在外部构造实例                                                                                        
    }                                                                                                           
}                                                                                                               
public class Demo26 {                                                                                           
    public static void main(String[] args) {                                                                    
        SingletonHungry t1 = SingletonHungry.getInstance();                                                     
        SingletonHungry t2 = SingletonHungry.getInstance();                                                     
        System.out.println(t1 == t2);                                                                           
                                                                                                                
        //SingletonHungry t3 = new SingletonHungry();报错                                                         
    }                                                                                                           
}                                                                                                               

饿汉模式顾名思义就是饥饿迫切的在类中构建一个实例(static静态变量,在JVM进行类加载的时候就创建) 由于是单例模式,类中就已经实例出来类的对象以后就通过类名来调用它的实例,构造方法修改为private不让外部再创建实例,由于获取实例是读取操作是原子的所以是天然的线程安全的

懒汉模式:

懒汉模式在类加载的时候不创建实例,在第一次使用的时候创建,如果一直不适用也就没必要创建了,省去了一些内存性能的开销

假设一个小说(千万字)肯定是只把一部分展示出来,后续如果用户翻页,随着翻页,随时就在后续数据,如果全部加载出来,可能会导致程序崩溃

打开编译器的把所有内容,都是从文件加载到内存中,再显示

复制代码
class SingletonLazy {
    private static SingletonLazy instance = null;

    //1
    public static SingletonLazy getInstance() {
        //这样有线程安全问题
        //在多个线程下由于这个操作不是原子的 会由于线程的随机调度引发创建实例覆盖的问题
        if (instance == null) {//防止new多个实例
            instance = new SingletonLazy();//创造时机是在第一次使用的时候 而不是在程序启动的时候(节省空间资源更推荐)
        }
        return instance;
    }
}

上述懒汉方式由于if(判断)读操作 和new实例(修改操作)涉及一个读操作一个写操作不是原子的在单线程中不涉及线程安全,在多线程中线程安全问题就很明显,如下↓

第一个线程new出来对象,由操作系统的调度器,调度到线程二开始执行new操作,随着第二个线程的覆盖操作,第一个new出来的对象随后会被JVM的垃圾回收 回收掉

两次new操作在数据很少的时候其实都无所谓,但是还是拿JDBC中new DataSource的时候数据内存很大可能new一个就要十分钟 如果出现两次new操作就会大大削减性能,甚至还会导致服务器崩溃

所以要解决

解决不是原子性问题肯定是要加锁

加上synchronized可以改善这⾥的线程安全问题.

复制代码
class SingletonLazy {
    private static SingletonLazy instance = null;

    private static Object locker = new Object();//锁对象

    public static SingletonLazy getInstance() {
        //为了解决原子性问题就要加锁
        /**
         * 引入锁之后 后执行的线程在加锁的位置阻塞 阻塞到前一个线程解锁
         * 当后一个线程进入条件的时候 前一个线程已经修改完毕
         * isnstance不会再为null 就不会后续的new
         * 两个线程两把锁(这里只有一把锁) 无法构成请求和保持 不会死锁
         */
        synchronized (locker) {//也可以再函数首部加锁
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }

        /**
         * 实例创建好后 后去再调用该方法 都是直接return 如果只是进行if(判定) + 读取return 就不涉及到线程安全的问题了
         * 但是每次调用上述方法 都会触发依次加锁的操作 虽然不涉及安全问题了
         * 多线程的情况下 这里的加锁 就会相互阻塞 影响程序的执行效率  
         */
        return instance;
    }

引入锁之后,后执行的线程就会在加锁的位置阻塞,一直阻塞到前一个线程解锁

当后一个线程进入条件的时候,前一个线程已经修改完毕 instance不再为null就不会进行后续的new操作

但是加入锁之后就引入了一个新的问题:

实例创建好后 后去再调用该方法 都是直接return

如果只是进行if(判定) + 读取return(纯读取的操作) 就不涉及到线程安全的问题了 但是每次调用上述方法 都会触发依次加锁的操作

虽然不涉及安全问题了 多线程的情况下 这里的加锁 就会相互阻塞 影响程序的执行效率 所以要改善效率问题

getInstance可以这样改

复制代码
public static SingletonLazy getInstance() {
        if (instance == null) {//判断是否需要加锁
            synchronized (locker) {
                if (instance == null) {//判断是否需要new对象 两个if在单线程中执行流只有一个 if判定结果一样
                                        //在多线程中其他线程可能就会把if中的instance变量给修改了也倒是两次的if结果的结论不同
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

在锁的外面再加一层if条件的判断来判断是否需要加锁,两个if执行的意义不一样可看代码注释

还有一些其他问题:

内存可见性,就不如再t1线程在读取instance的时候,t2线程进行修改,由于编译器的内存优化的逻辑非常复杂保险起见还是在instance上加一个volatile关键字,从根本上杜绝内存可见性问题

还有一个更关键的问题:指令重排序

指令重排序也是编译器优化的一种体现形式,编译器会在逻辑不变的前提下,调整你的代码顺序表来达到提升性能的效果

编译器优化往往不只是javacode自己的工作,通常是javac和JVM配和的效果(甚至是操作系统也要参与配合)

上述这个代码的机器指令中

复制代码
instance = new SingletonLazy();
复制代码
三步骤:1.申请内存空间 2.在空间上构造对象(初始化) 3.内存空间的首地址赋值给引用变量
* 正常来说按照123的顺序执行命令
* 但是在指令重排序的情况下会成为132这样的顺序 单线程123 132都一样 多线程可能有bug
* 可能会拿着未初始化的实例来进行操作

如下图指令重排序的bug

解决的话还是在instance 上加上volatile关键字预防指令重排序问题

复制代码
    private static volatile SingletonLazy instance = null;
复制代码
volatile的功能两方面
* 1.确保每次读取操作都是读内存
* 2.关于变量的读取和修改操作不会引起指令的重排序(主要)

三.工厂模式

我们先来看一个场景

在一个平面直角坐标系中描述一个点的位置,可以通过x,y两点的坐标直接确立,也可以通过极坐标的半径r和α角来确立.

复制代码
class Point {
    public Point(double x, double y) {
        //构造点的坐标
    }

    public Point(double r, double a) {
        //构造点的极坐标
    }
  
}
复制代码
上述两个构造方法是在平面直角坐标系中确定一个点
第一个方法是直角坐标系 第二个方法是极坐标系
两个方法构成冲突也不能重载(overload)

解决方法就是利用工厂方法

利用一个工厂类来提供工厂方法

复制代码
class Point {

}

class PointFactory {
    //工厂方法
    public static Point makePointByXY(double x, double y) {
        Point p = new Point();
        //通过x 和 y给p进行属性设置
        return p;//返回构造好的Point对象
    }

    public static Point makePointByRA(double r, double a) {
        Point p = new Point();
        //通过r 和 a给p进行属性设置
        return p;
    }
}

工厂方法的核心,通过静态方法,把构造对象new的过程各种属性初始化的过程给封装起来

提供多组静态方法 实现不同情况的构造

使用就可以这样使用 通过拿Point类来接受工厂方法的返回值也就类似于Point 来new实例了

复制代码
public class Demo {
    public static void main(String[] args) {
        Point p = PointFactory.makePointByXY(10, 20);
    }
}

上述只是一个工厂模式使用的案例:解决构造方法冲突 还有其他的作用,如下:

1.解耦对象的创建和使用

2.提高代码的可维护性和可扩展性

3.便于代码的重复使用

4.便于对象的管理和控制

当然也有缺点:复杂性增加,还需要权衡选择不要过度依赖,造成代码冗余