java设计模式之单例模式详解

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式、注册式(枚举式和容器式)以及ThreadLocal实现的单例。重点讨论了它们的定义、应用场景、代码示例,以及线程安全性和内存占用问题。
摘要由CSDN通过智能技术生成

单例模式定义及应用场景

使用单例模式,规定全局只能有一个实例,并且提供一个全局访问站点。单例模式在java中的应用场景很多,比如说JAVAEE的servletContext,spring的ApplicationContext,数据库连接池。

饿汉式单例模式

饿汉式单例模式也是提供一个全局实例,只是全局实例在初始化时就加载了,不管有没有用到。

代码示例

public class HungryStaticSingleton {
    //先静态后动态
    //先上,后下
    //先属性后方法
    private static final HungryStaticSingleton hungrySingleton;

    //装个B
    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

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

饿汉式单例模式定死了初始化时就做创建对象,适用于系统中单例对象较少的场景,由于定死了一个对象,所以在多线程的环境下是安全的。但是在单例对象较多的情况下会占用较大的内存空间。

懒汉式单例模式

因为懒汉式单例模式在类一创建时就会创建对象,当系统中的单例对象过多时,就会占用大量的内存空间。所以这边采用懒汉式单例模式来解决,懒汉式单例模式和饿汉式单例模式一样,构造方法私有化,在类中提供静态公有构造方法提供唯一实例。与饿汉式单例模式不同的是,懒汉式单例模式的对象要在使用的时候才创建。

代码示例

public class LazySimpleSingletion {
    private static LazySimpleSingletion instance;
    private LazySimpleSingletion(){}

    public static LazySimpleSingletion getInstance(){
        if(instance == null){
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

这种情况解决了系统运行就初始化对象的问题,但是存在线程安全问题。我们来开启两个线程来同时访问该实例。

public class LazySimpleSingletionExcutor implements Runnable{
    @Override
    public void run() {
        LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
        System.out.println(Thread.currentThread().getName()+": "+instance);
    }
}
public class TestLazySimpleSingletion {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new LazySimpleSingletionExcutor());
        Thread thread2 = new Thread(new LazySimpleSingletionExcutor());
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

在线程跑的位置打上线程断点,然后以debug形式运行。
在这里插入图片描述
在这里插入图片描述
可以发现线程1和线程0同时运行,势必会出现多线程安全问题问题。

测试运行结果:
在这里插入图片描述

这里只有两个线程,在线程量过多的情况下,是有可能出现多个不同的实例的。为了解决这种问题,我们可以在方法上加上synchronized关键字。

public class LazySimpleSingletion {
    private static LazySimpleSingletion instance;
    private LazySimpleSingletion(){}

    public  static synchronized LazySimpleSingletion getInstance(){
        if(instance == null){
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

再次运行代码,进入断点调试,可以发现一个进程进入getInstance方法时,另一个线程变成了monitor状态,发生了线程阻塞,这样就可以保证线程安全。可以发现线程安全问题得到解决。
在这里插入图片描述
但是当访问线程数量过多时,就会产生一个个的线程阻塞,导致我们程序的运行效率越来越慢,为了解决这个问题,我们采用双重锁机制。

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton getInstance(){
        //检查是否要阻塞
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                //检查是否要重新创建实例
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                    //指令重排序的问题
                }
            }
        }
        return instance;
    }
}

使用双重锁机制可以保证一个线程在在运行时,另一个线程也可以进入方法内部,在方法内部加锁也可以做到线程安全性。这种情况对性能的影响是肉眼观测不到的。
但是这种方式终归还是要上锁,对性能终归还是有一点影响,有没有什么方式是不要上锁也能保证线程安全类,答案就是采用静态类部类初始化的方式。

public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){
       
    }

    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

用这种方式,既保证了实例不会在创建时就加载,同时静态类部类会在方法调用之前初始化,所以也保证了线程安全问题,同时没有上锁,保证了程序运行的性能。
上面的方式看似是完美,但是金无足赤,人无完人,在牛逼的程序也会有漏洞,我们来用暴力反射获取上面静态类部类实现的构造方法,然后在通过newInstance方法获取实例,可以发现获取到的实例还是两个。

public class LazyStaticInnerClassSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<LazyStaticInnerClassSingleton> lazyStaticInnerClassSingletonClass = LazyStaticInnerClassSingleton.class;
        Constructor<LazyStaticInnerClassSingleton> declaredConstructor = lazyStaticInnerClassSingletonClass.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyStaticInnerClassSingleton lazyStaticInnerClassSingleton = declaredConstructor.newInstance();
        LazyStaticInnerClassSingleton lazyStaticInnerClassSingleton1 = declaredConstructor.newInstance();
        System.out.println(lazyStaticInnerClassSingleton1 == lazyStaticInnerClassSingleton);
    }
}

运行代码,发现:
在这里插入图片描述
这里显然是创建了两个实例,于是我我们在它的构造方法上添加异常处理。

public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){
        if(LazyHolder.INSTANCE != null){
            throw new RuntimeException("不允许非法访问");
        }
    }

    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }

}

在次运行代码,发现:
在这里插入图片描述
经过一步步的演进,看似最完美的版本总算诞生了。但是只要我们发散场景,上面这种方式的单例模式还是有可能被破坏。
对象的创建还有另外一种方式即序列化和反序列化,序列化就是将一个对象以文件的形式存储在磁盘中,等到对象要使用时将磁盘文件反序列化成对象来使用,但是两次创建的对象肯定不是同一个,这显然违背了单例模式的定义。

public class SeriableSingletonTest {
    public static void main(String[] args) {
        LazyStaticInnerClassSingleton singleton1 = null;
        LazyStaticInnerClassSingleton singleton2 = LazyStaticInnerClassSingleton.getInstance();
        String filePath = "D:\\code\\test\\testSingleton\\SeriableSingleton.obj";
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(filePath);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("D:\\code\\test\\testSingleton\\SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            singleton1 = (LazyStaticInnerClassSingleton) ois.readObject();
            ois.close();
            System.out.println(singleton1);
            System.out.println(singleton2);
            System.out.println(singleton1 == singleton2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

代码运行结果:
在这里插入图片描述
可以发现此时创建的对象不是同一对象,违反单例模式的原则。解决方式就是给单例对象加上readResolve方法

private Object readResolve() {
        return LazyHolder.INSTANCE;
    }

在看运行结果,结果如下:
在这里插入图片描述

注册式单例模式

注册式单例模式又称登记式单例模式,是将实例登记注册到某个地方,然后统一从发布处获取实例的方式,注册式单例模式有枚举式单例模式和容器式单例模式。

枚举式单例模式

枚举式单例模式是指将实例定义在枚举内部,通过枚举的一个实例方法进行获取的方式。

public 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;}
}

利用io序列化和反序列化的方式创建对象和使用对象测试。

public class EnumSingleTonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());
            FileOutputStream fos = new FileOutputStream("D:\\code\\test\\testSingleton\\Enumsingleton ,obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("D:\\code\\test\\testSingleton\\Enumsingleton ,obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述
由此可见,序列化和反序列化创建对象不能破坏枚举单例。那么反射能不能破坏枚举单例嘞,我们来试一试。

public class EnumSingleTonReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
        Constructor<EnumSingleton> constructor = enumSingletonClass.getDeclaredConstructor();
        EnumSingleton enumSingleton = constructor.newInstance();
        System.out.println(enumSingleton);
    }
}

运行结果如下:
在这里插入图片描述
这个异常很明显,没有找到空参构造方法,枚举是没有构造方法吗,我们来看看枚举的源码。

  protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

可以发现,枚举只有一个两个参数的构造方法,那么我们用它这个构造方法来创建一个对象试试。

public class EnumSingleTonReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
        Constructor<EnumSingleton> constructor = enumSingletonClass.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingleton enumSingleton = constructor.newInstance("tom",666);
        System.out.println(enumSingleton);
    }
}

运行结果还是有异常,异常是不能用反射来创建反射对象。
在这里插入图片描述
我们习惯性的点开Constructor的newInstance方法,可以发现很明显的对于枚举构造反射对象创建对象,直接就抛出异常了。

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

由此可见,枚举式单例不存在线程安全问题,序列化创建对象和反射创建对象都不能够支持,是目前最稳的单例模式实现。

容器式单例

枚举式单例虽然优雅,但是和饿汉式单例模式一样,在系统初始化的时候就已经将所有对象加载到内存中了。这种在单例对象过多的情况下并不适合使用。我们来看一下一种适合系统中存在多个单例对象的注册单例模式-容器单例模式。

public class ContainerSingleton {

    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            }catch (Exception e){
                e.printStackTrace();
            }
            return instance;
        }else{
            return ioc.get(className);
        }
    }

}

可以发现,容器式单例是用一个currentHashMap容器来进行管理的,它是非线程安全的。

ThreadLocal线程单例

ThreadLocal不能保证全局的单例,但是能保证一个线程只有一个实例。

public 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();
    }
}
public class ThreadLocalSingletonExecutorThread implements Runnable {

    @Override
    public void run() {
        ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ": " + instance);
    }
}

在这里插入图片描述

  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值