赞
踩
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
1、单例模式节省公共资源
比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。
对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。
2、单例模式方便控制
就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。
1. 构造私有:
如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。
2.以静态方法返回实例。
因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。
下面的代码案例中均是使用的getInstance()方法来返回实例。
3.确保对象实例只有一个。
只对类进行一次实例化,以后都直接获取第一次实例化的对象。
(饿汉模式->懒汉模式(与之平行的是内部类模式)->枚举类)
饿汉模式的意思是,我先把对象(面包)创建好,等我要用(吃)的直接直接来拿就行了。
- package single;
-
- // 饿汉单例(一开始就加载)
- public class a_Hungry {
-
- // 可能会浪费空间
- private byte[] data1 = new byte[1024 * 1024];
- private byte[] data2 = new byte[1024 * 1024];
- private byte[] data3 = new byte[1024 * 1024];
- private byte[] data4 = new byte[1024 * 1024];
-
- // 构造器私有化
- private a_Hungry() {
- }
-
- private final static a_Hungry HUNGRY = new a_Hungry();
-
- public static a_Hungry getInstance() {
- return HUNGRY;
- }
- }
上面的案例就是使用的饿汉模式。 这种模式是最简单最省心的,不足的地方是容易造成资源上的浪费(比如:我事先把面包都做好了,但是你并不一定吃,这样容易造成资源的浪费)。
比如代码中标注可能会浪费空间后面的那四条代码。无论你是不是要使用该类对象,data1-data4已经占用了内存空间。
因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式,
懒汉模式的意思是,我先不创建类的对象实例,等你需要的时候我再创建。
- package single;
-
- // 懒汉单例(多线程不安全)
- public class b_Lazy {
-
- // 构造器私有化
- private b_Lazy() {
- System.out.println(Thread.currentThread().getName() + "——OK");
- }
-
- private static b_Lazy b_lazy;
-
- public static b_Lazy getInstance() {
- if (b_lazy == null) {
- b_lazy = new b_Lazy();
- }
- return b_lazy;
- }
-
- // 多线程并发
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- b_Lazy.getInstance();
- }).start();
- }
- }
- }
但很明显,这种最简单的懒汉模式在多线程并发情况下,会出现线程不安全的问题。所以会有以下DCL模式改进。
- package single;
-
- // 懒汉单例(多线程不安全)
- public class b_Lazy_DCL {
-
- // 构造器私有化
- private b_Lazy_DCL() {
- System.out.println(Thread.currentThread().getName() + "——OK");
- }
-
- private static b_Lazy_DCL b_lazy;
-
- // 双重检测锁模式 懒汉式单例 CDL模式
- public static b_Lazy_DCL getInstance() {
- if (b_lazy == null) {
- synchronized (b_Lazy.class) {
- if (b_lazy == null) {
- b_lazy = new b_Lazy_DCL();
- }
- }
- }
- return b_lazy;
- }
-
- // 多线程并发
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- b_Lazy_DCL.getInstance();
- }).start();
- }
- }
- }
如代码中所示,在返回实例的方法getInstance()方法中,进行了双重检测,首先判断实例是否被创建,如果没有则会通过synchronized关键字来上锁,并开始创建实例。
但这种DCL模式下还是有指令重排的问题,什么是指令重排和如果解决,办法如下。
- package single;
-
- // 懒汉单例(多线程不安全)
- public class b_Lazy_DCL_issue {
-
- // 构造器私有化
- private b_Lazy_DCL_issue() {
- System.out.println(Thread.currentThread().getName() + "——OK");
- }
-
- private volatile static b_Lazy_DCL_issue b_lazy;
-
- // 双重检测锁模式 懒汉式单例 CDL模式
- public static b_Lazy_DCL_issue getInstance() {
- if (b_lazy == null) {
- synchronized (b_Lazy.class) {
- if (b_lazy == null) {
- b_lazy = new b_Lazy_DCL_issue(); // 不是原子性操作
- /*
- 问题
- 1、分配内存空间
- 2、执行构造方法,初始化对象
- 3、把这个对象指向这个空间(占用空间)
- 指令重排
- 理想状态:123
- 可能状态:132 A线程先到了3
- B线程判断为!null,直接return了,但b_lazy还没有完成构造。
- */
- // 解决办法:b_lazy避免指令重排加上volatile
- }
- }
- }
- return b_lazy;
- }
-
- // 多线程并发
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- b_Lazy_DCL_issue.getInstance();
- }).start();
- }
- }
- }
问题如注释,而解决指令重排的办法就是在属性前加上volatile关键字。
到此,在正常情况下是没问题的。但Java中著名的反射机制,依旧可以破坏上面的单例模式。
- package single;
-
- import java.lang.reflect.Constructor;
-
- // 如果初始化是正常初始化没有问题
- public class b_Lazy_DCL_reflect {
-
- // 构造器私有化
- private b_Lazy_DCL_reflect() {
-
- // 解决暴力反射破坏单例(第三把锁)
- synchronized (b_Lazy_DCL_reflect.class) {
- if (b_lazy != null) { // 已经创建了,这次是来搞破坏的反射
- throw new RuntimeException("异常——不要试图使用反射破坏单例");
- }
- }
- }
-
- private volatile static b_Lazy_DCL_reflect b_lazy;
-
- // 双重检测锁模式 懒汉式单例 CDL模式
- public static b_Lazy_DCL_reflect getInstance() {
- if (b_lazy == null) {
- synchronized (b_Lazy.class) {
- if (b_lazy == null) {
- b_lazy = new b_Lazy_DCL_reflect();
- }
- }
- }
- return b_lazy;
- }
-
- // 暴力反射
- public static void main(String[] args) throws Exception {
- b_Lazy_DCL_reflect instance1 = b_Lazy_DCL_reflect.getInstance();
-
- Constructor<b_Lazy_DCL_reflect> declaredConstructor = b_Lazy_DCL_reflect.class.getDeclaredConstructor(null);
- declaredConstructor.setAccessible(true); // 无视了私有的构造器
- b_Lazy_DCL_reflect instance2 = declaredConstructor.newInstance();
-
- System.out.println(instance1 == instance2);
-
- }
- }
在main方法中,instance1是规范下通过getInstance()方法创建实例,但instance2是在反射破坏掉单例模式下创建的,instance2是该类新的一个实例。
而解决办法就是在构造器中加上第三把锁,拒绝掉通过反射创建的新的实例对象。
但依旧会有漏洞,如果main方法中的第一个实例就是通过反射机制来创建的,上面的单例还是被破坏掉了。解决办法如下。
- package single;
-
- import java.lang.reflect.Constructor;
-
- // 简单三把锁被反射初始化破坏
- // 解决办法:通过标志位,强调必须用反编译允许程序
- public class b_Lazy_DCL_reflect2 {
-
- // 红绿灯
- private static boolean single = false;
-
- // 构造器私有化
- private b_Lazy_DCL_reflect2() {
- // 解决暴力反射破坏单例(第三把锁)
- synchronized (b_Lazy_DCL_reflect2.class) {
- if (single == false) {
- single = true;
- } else {
- throw new RuntimeException("异常——不要试图使用反射初始化破坏单例");
- }
- }
- }
-
- private volatile static b_Lazy_DCL_reflect2 b_lazy;
-
- // 双重检测锁模式 懒汉式单例 CDL模式
- public static b_Lazy_DCL_reflect2 getInstance() {
- if (b_lazy == null) {
- synchronized (b_Lazy.class) {
- if (b_lazy == null) {
- b_lazy = new b_Lazy_DCL_reflect2();
- }
- }
- }
- return b_lazy;
- }
-
- // 暴力反射
- public static void main(String[] args) throws Exception {
- // b_Lazy_DCL_reflect2 instance1 = b_Lazy_DCL_reflect2.getInstance(); // 还是被破坏掉了
- Constructor<b_Lazy_DCL_reflect2> declaredConstructor = b_Lazy_DCL_reflect2.class.getDeclaredConstructor(null);
- declaredConstructor.setAccessible(true); // 无视了私有的构造器
-
- b_Lazy_DCL_reflect2 instance1 = declaredConstructor.newInstance();
- b_Lazy_DCL_reflect2 instance2 = declaredConstructor.newInstance();
- System.out.println(instance1 == instance2);
-
- }
- }
这里的机制就是,反射是不会反编译该程序,直接跳到创建实例对象,因此也就不会改变标志位的内容。而正常情况下创建该类实例,则会改变标志位内容。
所有加上标志位single并在构造方法中判断标志位是否为true来判断第一次是否是通过反射创建实例。
但反射是强大的,你可以通过设置标志位来加密,也就有可能会出现通过反射来解密并且修改你的标志位内容来破坏你的单例。
解决反射的最终办法,通过枚举类来创建单例模式。
- package single;
-
- import java.lang.reflect.Constructor;
-
- // enum本身也是一个Class类
- public enum d_EnumSingle {
- INSTANCE;
-
- public d_EnumSingle getInstance() {
- return INSTANCE;
- }
- }
-
- class Test {
- public static void main(String[] args) throws Exception {
- d_EnumSingle instance1 = d_EnumSingle.INSTANCE;
- Constructor<d_EnumSingle> declaredConstructor = d_EnumSingle.class.getDeclaredConstructor(String.class, int.class);
- declaredConstructor.setAccessible(true);
-
- d_EnumSingle instance2 = declaredConstructor.newInstance();
-
- System.out.println(instance1 == instance2);
-
- }
- }
枚举类本身也是Class,枚举是一种特殊的数据类型,之所以特殊是因为它既是一种类(Class)类型却又比类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁,安全性以及便捷性。
代码中可见,已经通过反射来准备迫害单例,,但最终被枚举类的机制判断出,并进行了阻止。
- package single;
-
- // 静态内部类
- public class c_Holder {
-
- // 构造器私有化
- private c_Holder() {
- }
-
- public static c_Holder getInstance() {
- return InnerClass.HOLDER;
- }
-
- public static class InnerClass {
- private static final c_Holder HOLDER = new c_Holder();
- }
- }
静态内部类原理:
当外部内被访问时,并不会加载内部类,所以只要不访问InnserClass这个内部类,private static final c_Holder HOLDER = new c_Holder()就不会实例化,这就相当于实现懒加载的效果,只有当InnerClass.HOLDER被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了恶汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。
到此,单例模式算是基本说全了,其实在没有写真实项目前,我也一直没有明白保护单例模式的作用具体是什么,希望未来在工作中能够逐渐了解。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。