设计模式-创建型模式-单例模式

110 阅读8分钟

由来

单例模式属于创建型模式,它提供了一种创建对象的最佳方式。

单一的类,负责创建自己的对象,同时确保只有单个对象被创建。 这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,构造器私有

饿汉式单例

package com.mao.designPatterns.demo;

/**
 * @author mao
 * @createTime 2022/9/9 0009 16:24
 * @Description: 饿汉式单例
 */
public class SingletonObject {
    // 存在问题,在类一开始的时候就会创建对象,浪费空间
    byte[] a = new byte[1024*1024];
    byte[] b = new byte[1024*1024];
    byte[] c = new byte[1024*1024];

    // 饿汉式单例,在最开始就会创建一个对象
    private  final static SingletonObject singletonObject = new SingletonObject();

    // 构造器私有
    private SingletonObject(){}

    public static SingletonObject getInstance(){
        return singletonObject;
    }

    public void hello(){
        System.out.println("Hello World!");
    }
}

class Test{
    public static void main(String[] args) {
        SingletonObject instance = SingletonObject.getInstance();
        instance.hello();
    }
}

在这里插入图片描述

存在问题和思考

首先确认什么情况会导致线程不安全:

1.线程的调度是抢占式执行.

2.修改操作不是原子的

3.多个线程同时修改同一个变量

4.内存可见性

5.指令重排序

饿汉式单例,类加载时就初始化,一旦在类初始化的时候new出了多个空间,但是并没有使用,就造成了内存的浪费。

但是在多线程同时调用getInstance()时候,多线程同时访问同一个变量,没有构成多个线程同时修改同一个变量这一情况,所以说饿汉模式是线程安全的。

懒汉式单例

不安全

package com.mao.designPatterns.demo;

/**
 * @author mao
 * @createTime 2022/9/9 0009 16:45
 * @Description: 懒汉式单例
 */
public class LazyManSingle {
    // 构造器私有
    private LazyManSingle (){}

    private static LazyManSingle lazyManSingle;

    public static LazyManSingle getInstance(){
        // 在实类为空的时候进行 创建
        if (lazyManSingle == null){
            lazyManSingle = new LazyManSingle();
        }
        return lazyManSingle;
    }

    public void hello(){
        System.out.println("hello");
    }
}

class TestLazyMan{
    public static void main(String[] args) {
        LazyManSingle instance = LazyManSingle.getInstance();
        instance.hello();
    }
}

在这里插入图片描述

存在问题和思考

懒汉式,当程序启动之后并不会进行初始化,在什么时候调用什么时候初始化。单线程下没有问题,但是在多线程下,由于没有加锁,所以会存在访问问题。

要是代码从头到尾都没有调用getInstance(),实例化对象就不会存在,懒加载机制提高了效率,不用就不会实例化。

多线程下为什么不安全呢?

此种模式下,多线程在调用getInstance() 时候,做了一下四件事

  • 访问 lazyManSingle
  • 判断 lazyManSingle是否为 null
  • 如果为空,lazyManSingle = new LazyManSingle();
  • 返回 lazyManSingle 地址

这种情况下,多线程对同一个变量进行了修改,所以这种懒汉式单例是不安全的。

安全

    public static synchronized LazyManSingle getInstance(){
        // 在实类为空的时候进行 创建
        if (lazyManSingle == null){
            lazyManSingle = new LazyManSingle();
        }
        return lazyManSingle;
    }

第一次调用才进行了初始化,减少了空间内存的浪费。 在getInstance()方法上面加锁,上来直接锁住,保证多线程下的安全,但是效率很低,99% 情况下不需要同步。

优化DCL

DCLDouble Check Lock双重检查锁

public class LazyManSingle {
    // 构造器私有
    private LazyManSingle (){}

    private static LazyManSingle lazyManSingle;

    public static synchronized LazyManSingle getInstance(){
        // DCL 双重检锁
        if (lazyManSingle == null){
            synchronized (LazyManSingle.class){
                if (lazyManSingle == null){
                    lazyManSingle = new LazyManSingle();
                }
            }
        }
        return lazyManSingle;
    }

    public void hello(){
        System.out.println("hello");
    }
}

仍然存在问题,多线程下保证原子性、可见性、有序性

  • 原子性

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。 在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作。

保证原子性

  • 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
  • 通过 Lock 接口保障原子性。
  • 通过 Atomic 类型保障原子性。
  • 可见性

当一个线程修改了共享变量的值,其他线程能够看到修改的值。 Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

保证可见性

  • 通过 volatile 关键字标记内存屏障保证可见性。
  • 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
  • 通过 Lock 接口保障可见性。
  • 通过 Atomic 类型保障可见性。
  • 通过 final 关键字保障可见性
  • 有序性

即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。

如何保证有序性

  • 通过 synchronized关键字 定义同步代码块或者同步方法保障可见性。
  • 通过 Lock接口 保障可见性。

思考 i++是否安全? 答案:不安全 首先i++一共做了一下操作

int i = 0;
i++

step 1、主存 i = 0;
step 2、将 i 拷贝一份到自己的工作内存中
step 3、执行 i++ --> i = 1; 写入到自己的工作内存中
step 4、OS 将 i = 1; 刷回到主存中

注意synchronized保证step 1、step 2、step 3原子性,但是OS不确定何时将哪一个线程工作栈中的工作内存刷回到主存中。

比如线程 t1将 i 读取到自己的工作内存中,执行后 i = 1,此时,线程t2也执行同样操作,t2工作内存中 i = 1OS无论先刷回哪一个,都有问题。

可见性volatile关键字使用==总线锁定和MESI协议==保证变量可见性(缓存一致性问题),通过这两个协议保证了step3、step4的原子性,不可中断,CPU不会切换,期间将其他的线程置为失效,等做完之后再做刷回主存,此时i = 1,依旧不正确。

指令重排序,JVM会选择尽量选择执行快的,效率高的代码片段优先执行,单线程下指令重排序不会产生问题,但是在多线程下,一定会存在问题,因此要禁止指令重排序

编译器优化: 只有第一次读操作从内存中读取数据同时存放在CPU的寄存器中,因为从寄存器中读取数据速度远大于从内存中读取,所以后续的读操作就直接从寄存器中读取数据。

volatile的作用:保持内存可见性,禁止编译器进行某种场景的优化(一个线程在读,一个线程在写,修改对于读线程来说可能没有生效)

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

因为lazyManSingle = new LazyManSingle();不是原子性操作

完美的解决方法

package com.mao.designPatterns.demo;

/**
 * @author mao
 * @createTime 2022/9/9 0009 16:45
 * @Description: 懒汉式单例
 */
public class LazyManSingle {
    // 构造器私有
    private LazyManSingle (){
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private static volatile LazyManSingle lazyManSingle;

    public static synchronized LazyManSingle getInstance(){
        // DCL 双重检锁
        if (lazyManSingle == null){
            synchronized (LazyManSingle.class){
                if (lazyManSingle == null){
                    lazyManSingle = new LazyManSingle();
                }
            }
        }
        return lazyManSingle;
    }

    public void hello(){
        System.out.println("hello");
    }
}

class TestLazyMan{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{LazyManSingle.getInstance();}).start();
        }
    }
}

静态内部类

// 静态内部类
public class Holder {
    private Holder(){}

    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

探究

反射破坏DCL

class TestLazyMan{
    public static void main(String[] args) throws Exception {
        LazyManSingle instance = LazyManSingle.getInstance();
        Constructor<LazyManSingle> declaredConstructor = LazyManSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyManSingle lazyManSingle = declaredConstructor.newInstance();
        System.out.println(instance == lazyManSingle);// false
    }
}

构造器修改,变安全

    private LazyManSingle (){
        synchronized (LazyManSingle.class){
            if (lazyManSingle != null){
                throw new RuntimeException("反射破坏异常");
            }
        }
        System.out.println(Thread.currentThread().getName() + "ok");
    }

再次破坏,通过反射,两个对象都是反射new出来的

class TestLazyMan{
    public static void main(String[] args) throws Exception {
        Constructor<LazyManSingle> declaredConstructor = LazyManSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        
        LazyManSingle instance = declaredConstructor.newInstance();
        LazyManSingle lazyManSingle = declaredConstructor.newInstance();
        System.out.println(instance == lazyManSingle);
    }
}

加入标志位可以避免破坏单例

    private static boolean coffee_mao = false;

    private LazyManSingle (){
        synchronized (LazyManSingle.class){
            if (coffee_mao == false){
                coffee_mao = true;
            }else {
                throw new RuntimeException("反射破坏异常");
            }
        }
        System.out.println(Thread.currentThread().getName() + "ok");
    }

但是反射又可以通过获取字段进行破坏,又破坏了单例

class TestLazyMan{
    public static void main(String[] args) throws Exception {
        // 破坏字段的权限
        Field coffee_mao = LazyManSingle.class.getDeclaredField("coffee_mao");
        coffee_mao.setAccessible(true);

        // 破坏单例对象
        Constructor<LazyManSingle> declaredConstructor = LazyManSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyManSingle instance = declaredConstructor.newInstance();

        // 创建一个之后,把字段的值再改回来
        coffee_mao.set(instance,false);
        LazyManSingle lazyManSingle = declaredConstructor.newInstance();
        System.out.println(instance == lazyManSingle);
    }
}

枚举

package com.mao.designPatterns.demo;

import java.lang.reflect.Constructor;

/**
 * @author mao
 * @createTime 2022/9/9 0009 21:59
 * @Description: 枚举
 */
public enum MyEnum {
    INSTANCE;

    public MyEnum getInstance(){
        return INSTANCE;
    }
}

class TestEnum{
    public static void main(String[] args) throws Exception {
        MyEnum instance = MyEnum.INSTANCE;
        MyEnum instance2 = MyEnum.INSTANCE;
        System.out.println(instance2 == instance);
        Constructor<MyEnum> declaredConstructor = MyEnum.class.getDeclaredConstructor(String.class,int.class);
        MyEnum instance3 = declaredConstructor.newInstance();
        System.out.println(instance == instance3);

    }
}

问题:idea中枚举空参构造,反射破坏结果应该是Cannot reflectively create enum objects,结果发现报的错是没有空参构造器

在这里插入图片描述

jdk自带进行反编译

在这里插入图片描述

发现具有一个空参构造器,结果不是意料之中的异常

在这里插入图片描述

经过专业工具jad反编译java字节码文件,可以发现生成的是一个有参构造器,因此,在反射破坏时候,需要传入对应的字节码文件,可以看到如下结果,符合预期源码异常。

在这里插入图片描述