设计模式--单例模式

单例模式(Singleton Pattern) 是指确保一个类在任何情况下都绝对只有一个实例,并提供全局访问点。**单例模式是创建型模式。**J2EE标准中ServletContext、ServletContextConfig等,Spring框架应用中ApplicationContext、数据库的连接池等也都是单例形式。

饿汉单例模式在类加载的时候就立即初始化,并且创建单例对象。它是线程安全的,在线程还没出现以前就实例化了。

java 复制代码
class HungrySingleton{
    //先静态、后动态
    //先属性,后方法
    //先上后下
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
    }
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

另外一种写法,利用静态代码块的机制:

java 复制代码
class HungrySingleton{
    //先静态、后动态
    //先属性,后方法
    //先上后下
    private static final HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

缺点:所有对象类加载的时候就实例化了,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费

懒汉式单例模式,单例对象要在被使用时才会初始化,解决了饿汉式单例模式可能带来的内存浪费问题。

java 复制代码
/**
 * 懒汉式单例模式 在需要使用的时候在进行实例化
 */
class LazySimpleSingleton{
    private LazySimpleSingleton(){}
    
    public static LazySimpleSingleton lazy = null;
    public static LazySimpleSingleton getInstance(){
        if(lazy==null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
    
}

但是这种写法在多线程环境下,就会出现线程安全问题。

可以给getInstance方法加上synchronized关键字,使这个方法变成线程同步方法:

java 复制代码
class LazySimpleSingleton{
    private LazySimpleSingleton(){}

    public static LazySimpleSingleton lazy = null;
    public static synchronized LazySimpleSingleton getInstance(){
        if(lazy==null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }

}

synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批线程阻塞,从而导致程序性能下降。

双重检查锁的单例模式(double-checked locking):

java 复制代码
class LazySimpleSingleton{
    private LazySimpleSingleton(){}

    public volatile static LazySimpleSingleton lazy = null;
    public static synchronized LazySimpleSingleton getInstance(){
        if(lazy==null){
            synchronized (LazySimpleSingleton.class){
                if(lazy == null){
                    lazy = new LazySimpleSingleton();
                }
            }
        }
        return lazy;
    }
}

双重检查锁需要加上volatile关键字,

  • 禁止指令重排序:在没有volatile修饰的情况下,处理器可能会对指令进行重排序以提高性能。这可能导致某些线程在第一次条件判断之前看到的是一个非空但尚未完全初始化的对象,从而违反了单例模式的规则。使用volatile关键字可以防止这种情况的发生,确保每个线程看到的对象状态是一致的。
  • **确保可见性:**volatile关键字强制线程从主内存读取变量的最新值,而不是依赖本地的缓存副本。这样做的目的是确保不同线程之间对共享数据的更新和读取是可见的。
  • **确保有序性:**volatile同样有助于维护程序的执行顺序,尽管它本身不能保证原子性。在双重检查锁的场景中,volatile的使用有助于确保特定操作的顺序,如首先判断单例对象是否为空,然后再尝试创建新对象。

用到synchronized关键字总归要上锁,对程序性能还是存在一定的影响的。我们可以从类初始化的角度来考虑,采用静态内部类的方式

java 复制代码
/**
 *  采用静态内部类的方式
 */
class LazyInnerClassSingleton{
    //使用LazyInnerClassSingleton的时候,默认会先初始化内部类
    //如果没有使用,则内部类不加载
    private LazyInnerClassSingleton(){};
    public static final LazyInnerClassSingleton getInstance(){
        //返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

这种方式兼顾了饿汉单例模式的内存浪费问题和synchronized的性能问题,内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

反射破坏单例

上面介绍的单例模式的构造方法除了加上private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,在调用getInstance()方法,应该有两个不同的实例。以LazyInnerClassSingleton位例:

java 复制代码
public class Test {
    public static void main(String[] args) {
        
        try {
            Class<?> clazz = LazyInnerClassSingleton.class;

            //通过反射获取私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问
            c.setAccessible(true);
            
            //初始化
            Object o1 = c.newInstance();
            
            //调用了两次构造方法,相当于"new"了两次,犯了原则性错误
            Object o2 = c.newInstance();

            System.out.println(o1==o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

显然,创建了两个不同的实例。为此,我们可以在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。优化后的代码:

java 复制代码
class LazyInnerClassSingleton{
    //使用LazyInnerClassSingleton的时候,默认会先初始化内部类
    //如果没有使用,则内部类不加载
    private LazyInnerClassSingleton(){
        if (LazyHolder.LAZY != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    };
    public static final LazyInnerClassSingleton getInstance(){
        //返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

序列化破坏

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化的对象会重新分配内存,即重新创建。即相当于违背了单例模式的初衷,如下:

java 复制代码
/**
 * 反序列化导致破坏单例模式
 */
class SeriableSingleton implements Serializable{
    //序列化,就是将内存中的状态通过转换成字节码的形式
    //从而写入其他地方(可以是磁盘,网络I/O)
    
    //反序列化就是将 通过I/O流的读取,进而将读取的内容转换成Java对象
    //在转换的过程中会重新创建对象 new
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}


public class Test {
    public static void main(String[] args) {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1==s2);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

反序列化时,首先获取序列化的类 : desc( 可理解为单例类的class类,但它和JVM加载到内存中的单例class类有不同)因为如果我们的单例类在构造方法中通过实例不为空则抛出异常防止了反射破坏单例,那单例类是不允许再实例化的。而desc类却依然可以实例化。(当我们反序列化一个对象时,永远不会调用其类的构造函数,反序列化后的实例变量与序列化之前的实例变量相同,类变量与当前的类变量相同,如果反序列化时类未被加载则类变量为默认值。)

当我们通过反序列化readObject()方法获取对象时会去寻找readResolve()方法,如果该方法不存在则直接返回新对象,如果该方法存在则按该方法的内容返回对象,以确保如果我们之前实例化了单例对象,就返回该对象如果我们之前没有实例化单例对象,则会返回null

如何保证在序列化的情况下也能够实现单例模式呢?需要增加readResolve()方法。代码如下:

java 复制代码
/**
 * 反序列化导致破坏单例模式
 */
class SeriableSingleton implements Serializable{
    //序列化,就是将内存中的状态通过转换成字节码的形式
    //从而写入其他地方(可以是磁盘,网络I/O)

    //反序列化就是将 通过I/O流的读取,进而将读取的内容转换成Java对象
    //在转换的过程中会重新创建对象 new
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    @Serial
    private Object readResolve(){
        return INSTANCE;
    }
}

从JDK源码角度,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回

注册式单例模式

注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,一种为容器式单例模式。

  • 枚举式单例模式
java 复制代码
/**
 * 枚举式单例模式
 */
enum EnumSingleton{
    INSTANCE;
    private Object data;

    public Object getData(){
        return data;
    }
    public void setData(Object data){
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

public class Test {
    public static void main(String[] args) {
        EnumSingleton s1 = null;
        EnumSingleton s2 = EnumSingleton.getInstance();
        s2.setData(new Object());

        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(s1.getData());


            System.out.println(s2.getData());
            System.out.println(s1.getData()==s2.getData());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通过反编译工具Jad反编译EnumSingleton在是静态变量,是饿汉单例模式的实现。在反序列化中readObject()调用了readObject0()->readEnum()。

java 复制代码
public static final EnumSingleton INSTANCE = new EnumSingleton("INSTANCE", 0);

枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

如果通过反射破坏枚举单例模式,汇报java.lang.NoSuchMethodException异常(找不到无参的构造方法),测试代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c =  clazz.getDeclaredConstructor();
            c.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

反编译后,EnumSingleton的只有一个构造方法:

通过有参构造器,报java.lang.IllegalArgumentException异常,即不能用反射来创建枚举类型。在newInstance方法中做了强制判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。测试反射代码如下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c =  clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            c.newInstance("Tom",666);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  • 容器式单例模式

其实枚举式单例模式,虽然写法优雅,但是也会有一些问题,因为它在类加载之时就将所有的对象初始化防在类内存中,这其实跟饿汉式并无差异,不适合大量创建单例对象的场景。注册式单例模式的另一种写法,即容器式单例模式:

java 复制代码
class ContainerSingleton{
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).getDeclaredConstructor().newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else{
                return ioc.get(className);
            }
        }
    }
}

容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。

Spring中容器式单例模式的实现代码:

线程单例实现ThreadLocal

ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,是线程安全的。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是通过线程空间来实现隔离的。

java 复制代码
/**
 * 线程单例实现 ThreadLocal
 */
class ThreadLocalSingleton{
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

class ExectorThread implements Runnable {
    public void run() {
        Object singleton =   ThreadLocalSingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+":"+singleton);
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

JDK中一个经典的单例模式的应用,Runtime类,如下:

总结

单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。

相关推荐
弗拉唐6 分钟前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi7738 分钟前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3431 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀1 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20201 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深1 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong2 小时前
slice介绍slice查看器
java·ubuntu
牧竹子2 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar
数据小爬虫@2 小时前
如何利用java爬虫获得淘宝商品评论
java·开发语言·爬虫