JAVA架构师之路三:设计模式之单例模式

日头没有辜负我们,我们也切莫辜负日头。——沈从文

代码世界中也存在以下顺口溜:

我单身,我骄傲,我为国家省套套。
我单身,我自豪,我为祖国省橡胶。

单例模式虽然简单,但真正懂的内行的人并不多,今天挑战全网最全的经典设计模式之单例模式。

1. 单例模式

定义

确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
隐藏其构造方法
属于创建型设计模式

适用场景

确保任何情况下都绝对只有一个实例
ServletContext、ServletConfig、ApplicationContext、DBPool

2. 饿汉式单例

定义

系统初始化的时候就加载,不管有没有用到这个单例。

优点

执行效率高,性能高,没有任何的锁

缺点

某些情况下,可能会造成内存浪费
能够被反射破坏

代码

public class HungrySingleton {
    
    

    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton(){
    
    }

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

3. 懒汉式单例

定义

系统初始化的时候不创建实例,只有用到的时候才创建实例。

优点

节省了内存

缺点

synchronized造成性能低下
能够被反射破坏

3.1 方法加锁写法

代码

public class LazySingleton {
    
    

    private static LazySingleton singleton = null;

    private LazySingleton(){
    
    }


    /**
     * 版本1
     * @return
     */
    private synchronized LazySingleton getInstance() {
    
    
        if (null == singleton) {
    
    
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

3.2 代码块加锁写法

代码

public class LazySingleton {
    
    

    private static LazySingleton singleton = null;

    private LazySingleton(){
    
    }
    
    /**
     * 版本2 相比版本1优化一点点
     * @return
     */
    private  LazySingleton getInstance() {
    
    
        synchronized (LazySingleton.class) {
    
    
            if (null == singleton) {
    
    
                singleton = new LazySingleton();
            }
        }
        return singleton;
    } 
}

3.3 双重判断加锁写法

陷阱案例

public class LazySingleton {
    
    

    private static LazySingleton singleton = null;

    private LazySingleton(){
    
    }
    
    /**
     * 版本3 双重判断
     * @return
     */
    private  LazySingleton getInstance() {
    
    
        if (null == singleton) {
    
    
            synchronized (LazySingleton.class) {
    
    
                if (null == singleton) {
    
    
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

版本3看起来相比版本2优化了不少,但其实这种双重判断在生产环境有一个极大的漏洞陷阱,就是指令重排序,有需要了解的可以在评论区留言。解决方案也很简单,就是 volatile 关键字。它可以限制指令重排序。

正确写法

public class LazySingleton {
    
    

    private volatile static LazySingleton singleton = null;

    private LazySingleton(){
    
    }
    
    /**
     * 版本3 双重判断
     * @return
     */
    private  LazySingleton getInstance() {
    
    
        if (null == singleton) {
    
    
            synchronized (LazySingleton.class) {
    
    
                if (null == singleton) {
    
    
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

双重判断的优点:性能高了,线程安全了。
缺点:代码可读性极差,不够优雅。

3.4 静态内部类写法

利用JVM加载类的顺序,静态内部类,只有用到的时候外部类用到静态内部类的时候才会加载。

优点

写法优雅,利用了Java的语法特点,性能高,避免了内存浪费

缺点

能够被反射破坏

public class LazyStaticInnerSingleton {
    
    

    private LazyStaticInnerSingleton(){
    
    }

    public static LazyStaticInnerSingleton getInstance() {
    
    
        return LazyHolder.INSTANCE;
    }

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

这种写法本来应该够优雅,够完美,但是却有一个缺点是能被反射破坏,文章最后我会证明什么是能被反射破坏。那有没有写法能让这个单例不会被反射破坏?答案是有的!

public class LazyStaticInnerSingleton {
    
    

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

    public static LazyStaticInnerSingleton getInstance() {
    
    
        return LazyHolder.INSTANCE;
    }

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

这种写法就解决了被反射破坏的问题。但是看起来不是那么的优雅。

4. 注册式单例

定义

将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。

4.1. 枚举写法注册式单例

优点

写法优雅,线程安全

缺点

和饿汉式类似,大量使用会造成内存浪费,根本原因在于枚举本身的特点。

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

使用方法

public class Test {
    
    

    public static void main(String[] args) {
    
    
        EnumSingleton singleton = EnumSingleton.getInstance();
        singleton.setData(new Object());
        singleton.getData();
    }
}

4.2. Spring IOC容器注册式单例

Spring设计者结合枚举式单例的写法和特点,写了一种自己的IOC 容器注册式单例。

public class ContainerSingleton {
    
    

    private ContainerSingleton() {
    
    }

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

    public Object getInstance(String className) {
    
    
        if (!ioc.containsKey(className)) {
    
    
            Object instance = null;
            try {
    
    
                instance = Class.forName(className).newInstance();
            } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
    
    
                e.printStackTrace();
            }
            return instance;
        } else {
    
    
            return ioc.get(className);
        }
    }
}

5. ThreadLocal单例

ThreadLocal单例肯定会用到ThreadLocal,根据ThreadLocal本身的特点,即同一线程内数据可见,那么这种单例就有本身的局限性,使用的很少。我曾经在token登陆的时候用到过。即前端会传一个token到后端,token能解析出登陆用户的信息。把解析后的信息放在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();
    }
}

6. 反射破坏单例证明

public class Test1 {
    
    
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
        Class clazz = HungrySingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();
        System.out.println(o1 == o2);//会输出false
    }
}

解决方案就是:构造方法抛异常。

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

7. 高高高手需要知道的-序列化破坏单例

首先你必须知道什么是序列化。序列化就是JVM内存中的对象,序列化到磁盘文件,再读取到内存,不同进程的数据交互需要序列化才能传输。
以上的所有单例模式,解决了各种各样的问题,但都存在同一个问题,就是都会被序列化破坏。意思就是:系统中的单例,被序列化到磁盘,然后再加载到内存,那么这序列化前后两个单例,并不是同一个单例。这就是序列化破坏单例。
解决方案:在单例中加入以下方法:

private Object readResolve() {
    
    
    // instead of the object we're on,
    // return the class variable INSTANCE
    return INSTANCE;
}

感谢您阅读本文,如果您觉得文章写的对您有用的话,请您点击上面的“关注”,点个赞,这样您就可以持续收到《JAVA架构师之路》的最新文章了。文章内容属于自己的一点点心得,难免有不对的地方,欢迎在下方评论区探讨,你们的关注是我创作优质文章的动力。

猜你喜欢

转载自blog.csdn.net/cuixhao110/article/details/109723891