第二章创建模式—单例设计模式


单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 所谓的单例模式保证某个类在程序中有且只有一个对象,类比现实生活中的地球类,只有一个地球对象

单例模式的结构

如何控制只有一个对象呢

  • 我们创建一个对象是通过该类的构造方法进行创建对象
  • 我们不能让外部进行创建对象,不然谁都能创建对象,那么就保证不了单例,那么我们的构造方法必须是private修饰,来控制对象的创建,也就是私有的构造方法
  • 外部不能创建对象,那么这个创建唯一的对象的任务也就会在我们单例类来进行实现

怎么设计这个类的内部对象

  • 首先我们知道在外部是创建不了对象的,所以这个对象的类型肯定不能是成员的,必须是静态的,通过类调用
  • 因为成员变量的属性是通过我们的对象进行赋值,我们只有一个对象,如果是成员变量,就先要有对象才能赋值,而赋值了才有那个唯一的对象,就出现了死循环

外部怎么访问

  • 一般属性我们用private进行修饰,为了实现其封装性
  • 直接通过公开的方法获取这个唯一的对象

单例模式的主要有以下角色

  • 单例类。只能创建一个实例的类
  • 访问类。使用单例类

单例模式的实现

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式 1:静态变量

public class Singleton {
    //1定义静态成员变量,在类加载时创建
    private static Singleton singleton=new Singleton();
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static Singleton getSingleton() {
        return singleton;
    }
}
  • 因为singleton是类成员变量,在我 Singleton类加载过程中的初始化过程中进行创建对象赋值,因为只会执行一次,所以是天然的线程安全
  • 缺点:singleton 对象是随着类的加载而创建的,如果该对象很大,却一直没有使用就会造成内存的浪费

饿汉式 2:静态代码块

该方式在成员位置声明 Singleton 类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。

public class Singleton {
    //1定义静态成员变量,在类加载时创建
    private static Singleton singleton;
    static {
        singleton=new Singleton();
    }
    //2私有化构造方法
    private Singleton(){
        
    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static Singleton getSingleton() {
        return singleton;
    }
}

该方式和饿汉式的方式 1 基本一样,所以该方式也存在内存浪费问题。

懒汉式 1:线程不安全

该方式在成员位置声明 Singleton 类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?

当调用 getInstance() 方法获取 Singleton 类的对象的时候才创建 Singleton 类的对象,这样就实现了懒加载效果。

public class Singleton {
    //1定义静态成员变量,在类加载时创建
    private static Singleton singleton;
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static Singleton getSingleton(){
        if(singleton==null){
            singleton=new Singleton();
        }
        return singleton;
    }
}

测试

public class Client {
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            System.out.println(Singleton.getSingleton());
        });
        thread1.start();
        System.out.println(Singleton.getSingleton());
    }
}
//com.lsc.itheima.pattern.singleton.demo3.Singleton@3d075dc0
//com.lsc.itheima.pattern.singleton.demo3.Singleton@6b047ec
  • 发现不是同一个对象,没有实现其单例的效果
  • 为什么不是线程安全的
    • 比如我们的thread1线程调用了getSingleton,进行if(singleton==null)判断,进入了判断体
    • 然后我们的main线程抢占了CPU,进行运行,进行if(singleton==null)判断,因为thread1线程并没有进行创建对象,也进入了判断体
    • 故两个线程各自创建了对应的对象

懒汉式 2:线程安全—方法级上锁

在懒汉式 1 的基础上,使用 synchronized 关键字对getSingleton这个方法进行加锁

public class Singleton {
    //1定义静态成员变量,在类加载时创建
    private static Singleton singleton;
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static synchronized Singleton getSingleton(){
        if(singleton==null){
            singleton=new Singleton();
        }
        return singleton;
    }
}
  • 但是该方法的执行效率特别低。
    • 懒汉模式中加锁的问题,对于 getSingleton 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
  • 注意:其实只有在初始化singleton的时候才会出现线程安全问题,一旦初始化完成就不存在了。

懒汉式 3:双重检查锁🚀

public class Singleton {
    //1定义静态成员变量,在类加载时创建
    private static Singleton singleton;
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static  Singleton getSingleton(){
        //第一次判断,如果singleton不为null,不进入抢锁阶段,直接返回实例
        if(singleton==null){
            synchronized (Singleton.class) {
                // 第二次判断,抢占到锁以后再次判断
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

  • 要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。
  • 使用volatile关键字保证单例对象初始化不会被中断,保证其他线程获得的对象一定是初始化完成的对象
    • 比如现在t1线程执行到了初始化的实例对象,正在执行new操作,还没完全结束(但是LazySingleTon!=null),然后t2线程执行到了判断唯一的对象是否为空,对于t2发现不为空,就直接返回了,但是返回的对象是没有完全初始化的,所以需要加volatile,就相当加了一层内存屏障,保证其他线程返回的对象必须等操作完全结束才能执行return语句
public class Singleton {
    //1定义静态成员变量,在类加载时创建
    // 声明Singleton类型的变量,使用volatile保证可见性和有序性
    private static volatile Singleton singleton;
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static  Singleton getSingleton(){
        // 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
        if(singleton==null){
            synchronized (Singleton.class) {
                // 第二次判断,抢占到锁以后再次判断
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

懒汉式 4:静态内部类🚀

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class Singleton {
    //2私有化构造方法
    private Singleton(){

    }
    // 定义一个静态内部类
    private static class SingletonHolder {
        // 在内部类中声明并初始化外部类的对象
        private static final Singleton singleton = new Singleton();
    }
    // 提供公共的访问方式
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

第一次加载 Singleton 类时不会去初始化 singleton,只有第一次调用 getInstance(),虚拟机加载 SingletonHolder 类并初始化 singleton,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

饿汉式 3:枚举🚀

首先,枚举方式是饿汉式单例模式,如果不考虑浪费内存空间的问题,这是极力推荐的单例实现模式。

因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式。

枚举的写法非常简单,而且枚举方式是所用单例实现中唯一一种不会被破坏的单例实现模式。

public class SingleTon {
    /**
     * 饿汉式:枚举实现
     */
    public enum Singleton {
        INSTANCE
    }
}

存在的问题

有两种方式可以使上面定义的单例类可以创建多个对象(枚举方式除外),分别是序列化反射

枚举方式是利用了 Java 特性实现的单例模式,不会被破坏,其他实现方式都有可能会被破坏

序列化破坏单例

问题演示:下面代码运行结果是false,表明序列化和反序列化破坏了单例设计模式。

public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
	     writeObject2File();
        Singleton s1 = readObjectFormFile();
        Singleton s2 = readObjectFormFile();
//        System.out.println(s1 == s2); // false
    }
    //从文件读取数据
    public static  Singleton readObjectFormFile() throws IOException, ClassNotFoundException {
        //创建对象输入流对象
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\a.txt"));
        //2.读取对象
        Singleton singleton =(Singleton) objectInputStream.readObject();
        //3释放资源
        objectInputStream.close();
        return singleton;
    }
    //向文件写数据
    public static void writeObject2File() throws IOException {
        //1获取Singleton对象
        Singleton singleton = Singleton.getSingleton();
        //创建对象输出流对象
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\a.txt"));
        // 3.写对象
        objectOutputStream.writeObject(singleton);
        // 4.释放资源
        objectOutputStream.close();
    }
}

解决方案:在 Singleton 类中添加readResolve()方法。

在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。

/**
 * 双重加锁方式(解决序列化破解单例模式)
 */
public class Singleton implements Serializable {
    //1定义静态成员变量,在类加载时创建
     声明Singleton类型的变量,使用volatile保证可见性和有序性
    private static volatile Singleton singleton;
    //2私有化构造方法
    private Singleton(){

    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static  Singleton getSingleton(){
        // 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
        if(singleton==null){
            synchronized (Singleton.class) {
                // 第二次判断,抢占到锁以后再次判断
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    /**
     * 下面是为了解决序列化反序列化破解单例模式
     * 当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
     */
    private Object readResolve() {
        return getSingleton();
    }

}

反射破坏单例

public class Client {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
       //1 获取Singleton的字节码对象
        Class<Singleton> singletonClass = Singleton.class;
        //2获取无参构造方法对象
        Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
        //3取消访问检查
        declaredConstructor.setAccessible(true);
        // 4.创建Singleton对象
        Singleton s1 = (Singleton) declaredConstructor.newInstance();
        Singleton s2 = (Singleton) declaredConstructor.newInstance();
        System.out.println(s1 == s2); // false
    }
}

解决方案:当通过反射方式调用构造方法进行创建时,直接抛异常

public class Singleton {
    //1定义静态成员变量,在类加载时创建
     声明Singleton类型的变量,使用volatile保证可见性和有序性
    private static volatile Singleton singleton;
    private static boolean flag = false;
    //2私有化构造方法
    // 私有构造方法
    private Singleton() {
        synchronized (Singleton.class) {
            // 如果是true,说明非第一次访问,直接抛一个异常,如果是false,说明第一次访问
            if (flag) {
                throw new RuntimeException("不能创建多个对象");
            }
            flag = true;
        }
    }
    // 3.提供一个公共的访问方式,让外界获取该对象
    public static  Singleton getSingleton(){
        // 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
        if(singleton==null){
            synchronized (Singleton.class) {
                // 第二次判断,抢占到锁以后再次判断
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

JDK 源码 - Runtime 类

Runtime 类使用的就是单例设计模式。

源码查看:下面是 Runtime 类的源码,是静态变量方式的饿汉单例模式。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
}

/**
 *  RuntimeDemo
 */
public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
        // 获取Runtime类的对象
        Runtime runtime = Runtime.getRuntime();
        // 调用runtime的方法exec,参数要的是一个命令
        Process process = runtime.exec("ifconfig");
        // 调用process对象的获取输入流的方法
        InputStream is =  process.getInputStream();
        byte[] arr = new byte[1024 * 1024 * 100];
        // 读取数据
        int len = is.read(arr); // 返回读到的字节的个数
        // 将字节数组转换为字符串输出到控制台
        System.out.println(new String(arr, 0, len, "GBK"));
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里不会投三分

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值