设计模式—单例设计模式

1.概述

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

2.结构

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

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

3.实现

3.1 预加载(饿汉式)

预加载:顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。

package com.lxg.pattern.singleton;

/**
 * 饿汉式:预加载
 * 静态成员变量
 */
public class Singleton1 {
    //定义静态成员变量获取本类的实例
    private static final Singleton1 INSTANCE = new Singleton1();

    //私有构造方法,避免通过new关键字来实例化对象,保证只存在一个实例
    private Singleton1(){}

    //提供一个公共的访问类,让外界获取该对象
    public static Singleton1 getINSTANCE() {
        return INSTANCE;
    }
}

第二种初始化实例方式:使用静态代码块

package com.lxg.pattern.singleton;

/**
 * 饿汉式的第二种实现方式:
 * 使用静态代码块的方式
 */
public class Singleton2 {

    //声明Singleton2类型的静态常量成员变量,但是没有初始化,在静态代码块中对其进行初始化
    private static final Singleton2 INSTANCE;

    //在静态代码块中对成员常量初始化
    static {
        INSTANCE = new Singleton2();
    }
    //私有化构造方法,避免外部使用new关键字创建实例
    private Singleton2(){}

    //定义公共方法,对外提供获取单例实例的接口
    public static Singleton2 getINSTANCE() {
        return INSTANCE;
    }
}

小结:
很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

3.2 预加载(饿汉式)——枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

package com.lxg.pattern.singleton;

/**
 * 枚举方式实现单例
 * 枚举类型实现单例模式是极力推荐的实现模式。因为枚举是线程安全的,并且只会装载一次,设计者充分
 * 利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一不会被
 * 破坏的单例实现模式。
 *
 * 枚举类型属于饿汉式方式,即预加载模式,在不考虑内存空间的情况下,使用枚举方式是比较好的一种方式
 */
public enum Singleton6 {
    INSTANCE;
}

3.3 懒加载(懒汉式)

为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

package com.lxg.pattern.singleton;

/**
 * 懒汉式 懒加载 lazy loading
 */
public class Singleton3 {
    //声明Singleton类型的变量
    private static Singleton3 instance; //只是声明,并没有赋值

    //私有化构造方法
    private Singleton3(){}
  
    /**
     * 第一种方式:
     *  对外提供获取单例的接口方法,下面这种方法存在线程安全问题。
     */
    public static Singleton3 getInstance() {
        if(instance==null){
            instance = new Singleton3();
        }
        return instance;
    }
}

从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

3.4 懒加载(懒汉式)——解决线程安全问题

通过在getInstance()方法上添加了synchronized关键字来解决线程安全问题:

package com.lxg.pattern.singleton;

/**
 * 懒汉式 懒加载 lazy loading
 */
public class Singleton3 {
    //声明Singleton类型的变量
    private static Singleton3 instance; //只是声明,并没有赋值

    //私有化构造方法
    private Singleton3(){}

    /**
     * 第二种方式:
     * 使用synchronized同步关键字来解决线程安全问题
     * synchronized加载getInstace()函数上确实保证了线程的安全。
     * 但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。
     * @return
     */
    public static synchronized Singleton3 getInstance() {
    	if(instance==null){
            instance = new Singleton3();
        }
        return instance;
    }
}

分析

上面代码的方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

3.5 懒加载(懒汉式)——双重检查

再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

package com.lxg.pattern.singleton;

import java.io.Serializable;

/**
 * 懒汉式的双重检查锁方式
 *
 */
public class Singleton4 implements Serializable {

    //使用volatile 关键字可以保证可见性和有序性
    private static volatile Singleton4 instance;

    private Singleton4(){}

    public static Singleton4 getInstance() {
        /**
         * 使用双重检查解决了单例、性能、线程安全问题,但是在多线程环境中由于JVM在实例化对象的时候会进行
         * 优化和指令的重排序操作,可能会导致空指针异常问题,这时候需要使用volatile关键字,可以保证可见性
         * 和有序性
         */
        //第一次判断,如果instance的值不为null,不需要抢占锁,再返回对象
        if(instance==null){
            synchronized (Singleton4.class){
                //第二次判断
                if(instance==null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

分析

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。
添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

3.6 懒加载(懒汉式)——静态内部类

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

package com.lxg.pattern.singleton;

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

    private static boolean flag = false;

    //私有化构造器
    private Singleton5(){
    }

	//定义静态内部类,私有化属性
    private static class SingletonHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    //对外提供静态方法获取该对象
    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

分析

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

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

4.存在的问题及解决方法

4.1 问题一:序列化和反序列化能够破坏单例模式

问题演示

实现Serializable接口,使得单例类可以序列化

package com.lxg.pattern.singleton;

import java.io.Serializable;

/**
 * 懒汉式的双重检查锁方式
 *
 */
public class Singleton4 implements Serializable {

    //使用volatile 关键字可以保证可见性和有序性
    private static volatile Singleton4 instance;

    private Singleton4(){}

    public static Singleton4 getInstance() {
        /**
         * 使用双重检查解决了单例、性能、线程安全问题,但是在多线程环境中由于JVM在实例化对象的时候会进行
         * 优化和指令的重排序操作,可能会导致空指针异常问题,这时候需要使用volatile关键字,可以保证可见性 和有序性
         */
        //第一次判断,如果instance的值不为null,不需要抢占锁,再返回对象
        if(instance==null){
            synchronized (Singleton4.class){
                //第二次判断
                if(instance==null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

通过序列化和反序列化能够破坏单例

package com.lxg.pattern.singleton;

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * 测试类
 */
public class Client {
    public static void main(String[] args) throws Exception {
        /**
         * 使用序列化破坏单例模式
         */
        //写对象
        writeObject2File();
        //读对象
        readObjectFromFile();
        readObjectFromFile();


    }

    /**
     * 向文件中写对象
     * @throws Exception
     */
    public static void writeObject2File() throws Exception{
        //获取Singleton对象
        Singleton4 instance = Singleton4.getInstance();
        //创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
        //写对象
        oos.writeObject(instance);
        //释放资源
        oos.close();
    }

    public static void readObjectFromFile() throws Exception {
        //创建对象输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
        //读取对象
        Singleton4 instance = (Singleton4) ois.readObject();

        System.out.println(instance);
        //释放资源
        ois.close();
    }

}

运行结果,明显不是同一个实例,破坏了单例模式

com.lxg.pattern.singleton.Singleton4@568db2f2
com.lxg.pattern.singleton.Singleton4@378bf509

问题解决

在类中实现readResolve()方法可以解决序列化和反序列化破坏单例的问题,因为在序列化的方法readObject()中定义会对类进行检查,检查是否有定义readResolve()方法,如果有则返回该方法的返回值作为反序列化的对象。

package com.lxg.pattern.singleton;

public class Singleton5 {

    private static boolean flag = false;

    //私有化构造器
    private Singleton5(){}

    private static class SingletonHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    //对外提供静态方法获取该对象
    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 定义此方法,当反序列化的时候,将该方法的返回值直接返回,解决
     * 了序列化和反序列化破坏单例的问题。
     * @return
     */
    public Object readResolve(){
        return SingletonHolder.INSTANCE;
    }
}

4.2 问题二:反射能够破坏单例模式

问题演示

通过反射来创建单例对象的演示

package com.lxg.pattern.singleton;

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * 测试类
 */
public class Client {
    public static void main(String[] args) throws Exception {
        /**
         * 通过反射破坏单例模式
         */
        destroySingletonByReflect();//返回结果为false,说明单例模式被破坏了

    }

    /**
     * 通过反射来破坏单例模式
     */
    public static void destroySingletonByReflect() throws Exception {
        //获取Singleton的字节对象
        Class<Singleton5> clazz = Singleton5.class;
        //获取无参构造方法
        Constructor<Singleton5> constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);
        //创建Singleton对象
        Singleton5 s1 = constructor.newInstance();
        Singleton5 s2 = constructor.newInstance();
        System.out.println("s1==s2? : "+(s1==s2));
    }
}

运行结果

false

问题解决

在单例类中对构造器进行限制,当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

package com.lxg.pattern.singleton;

public class Singleton5 {

    private static boolean flag = false;

    //私有化构造器
    /**
     * 在构造方法中增加判断,解决反射破坏单例的情况
     */
    private Singleton5(){
        synchronized(Singleton5.class){
            if(flag){
                //判断flag的值,如果是true,说明是第一次访问,如果不是,说明是第一次访问。
                throw new RuntimeException("不能创建两个对象");
            }
            //将flag的值设置为true
            flag = true;
        }
    }

    private static class SingletonHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    //对外提供静态方法获取该对象
    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

5.JDK源码解析-Runtime类

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

5.1 Runtime类部分源码

public class Runtime {
	//私有化自身类型的属性变量并初始化,属于饿汉式的单例模式
    private static Runtime currentRuntime = new Runtime();

	//定义接口方法,给外部获取单例实例
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    //私有化构造器,禁止外部通过new关键字来实例化对象
    private Runtime() {}
}

5.2 Runtime类的使用

public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
        //获取Runtime类对象
        Runtime runtime = Runtime.getRuntime();

        //返回 Java 虚拟机中的内存总量。
        System.out.println(runtime.totalMemory());
        //返回 Java 虚拟机试图使用的最大内存量。
        System.out.println(runtime.maxMemory());

        //创建一个新的进程执行指定的字符串命令,返回进程对象
        Process process = runtime.exec("ipconfig");
        //获取命令执行后的结果,通过输入流获取
        InputStream inputStream = process.getInputStream();
        byte[] arr = new byte[1024 * 1024* 100];
        int b = inputStream.read(arr);
        System.out.println(new String(arr,0,b,"gbk"));
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值