深入揭秘《单例模式》

深入揭秘单例模式

 单例模式是开发中频繁使用的设计模式之一,也是入门设计模式比较简单的一种设计模式。基本上开发者都会使用多种方式写单例模式,并且知道单例模式不同方式的优缺点。实际上,简单的单例模式也有其中的奥妙。


注意:本文重点讨论单例模式内部原理

1.懒汉式单例模式

懒汉式基本写法:

/**
 * 懒汉式 单例模式
 * @author jackcheng (jackcheng1117@163.com)
 */
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton () {}

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

懒汉式特点:

1. 顾名思义比较“懒”’,反应到Java程序中就是实现了懒加载(Lazy Loader)

2.尽管将构造器置为private,但是通过反射依然可以创建出对象  (后续不再列出)

3. 多线程下有线程安全问题


    原因在于判断中的   instance = new LazySingleton()
    实例化LazySingleton对象不是一个原子操作,大致可以分为3个步骤:
        1.为 new LazySingleton()这个对象分配内存
        2.初始化这个对象
        3.将instance引用指向刚才为new LazySingleton()这个对象分配的内存
    
   假设现有两个线程A、B
   A线程首先调用getInstance()方法获取实例,然后执行 instance == null判断,此时instance对象为null,

   所以A线程继续执行 instance = new LazySingleton(),开始初始化instance对象。当执行到第二
   步 ‘初始化这个对象’时,B线程也调用getInstance()方法,此时由于A线程还没有执行到第三步,所以B线程
  也可以通过if判断,那么就会造成A、B线程会实例化出两个LazySingleton对象。
  

   懒汉式单例模式在并发下创建多个对象,这完全违背了单例模式的初衷,解决方式如下:

   

/**
 * 懒汉式 单例模式
 * @author jackcheng (jackcheng1117@163.com)
 */
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton () {
        if (instance != null) {
            throw new RuntimeException("单例构造器禁止使用");
        }
    }

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

    这种方式没有什么安全问题,但是由于使用了synchronized锁,所以会影响性能。不推荐使用。

2.饿汉式单例模式

   饿汉式单例模式基本写法:

  

/**
 * 饿汉式  单例模式
 * @author jackcheng (jackcheng1117@163.com)
 */
public class HungarySingleton {

    private static final HungarySingleton INSTANCE = new HungarySingleton();

    private HungarySingleton() {}

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

   饿汉式特点:

   1. 没有实现懒加载

   2. 多线程下线程安全

  饿汉式除了没有实现懒加载,其他方面还是比较优秀的,开发中可以大胆使用。

 

3. Double Check Lock (双端检锁) 单例模式

   基本写法:

  

/**
 * Double Check Lock Singleton
 * @author jackcheng (jackcheng1117@163.com)
 */
public class DoubleCheckLockSingleton {

    private volatile static DoubleCheckLockSingleton instance = new DoubleCheckLockSingleton();

    private DoubleCheckLockSingleton() {}

    public static DoubleCheckLockSingleton getInstance() {
        if (instance == null) {
            synchronized(DoubleCheckLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }
}

  双端检锁单例模式特点:

   1. 实现懒加载

   2.并发下无线程安全问题  (必须加 volatile关键字)

 注意事项:(volatile关键字不再本文详细展开描述,有兴趣可以后续关注笔者文章)

使用双端检锁单例模式时,必须要加 volatile 关键字:

因为  instance = new DoubleCheckLockSingleton();
这行代码不是原子操作,JVM底层有可能会对其进行优化,也就是所说的指令重排(不是一定重排,是有一定概率的)

对于单线程来说,进行指令重排不是影响最后的结果,而且可以进行优化。但是对于多线程来说,可能就会存在安全问题。

 

instance = new DoubleCheckLockSingleton();
      这行代码的创建,可以大致分为3个步骤:
        1.给这个对象new DoubleCheckLockSingleton()分配内存
        2.初始化这个对象
        3.设置instance指向刚分配的内存地址

 

若我们不加  volatile关键字,可能会进行指令重排,假设重排为:

       1.给这个对象new DoubleCheckLockSingleton()分配内存
       2.设置instance指向刚分配的内存地址
       3.初始化这个对象

 

若重排为这种顺序,那么当执行到第二步时,就将instance指向了内存地址,但是此时 instance对象还没有完成初始化
也就是说,此时instance对象是不可用的。那么如果此时又来了一个线程,会首先检测  if(instance == null);
由于上个线程已经将instance指向了内存地址,所以结果为false,那么就会直接返回  instance对象,

但是此时第一个线程还没有完成对象初始化,第二个线程已经获得instance对象, 这样就会出现异常。

 

 

4. 静态内部类单例模式

   基本写法:

/**
 * 静态内部类 单例模式
 * @author jackcheng (jackcheng1117@163.com)
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }

    private static class InnerClass {
        public static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }
}

   静态内部类单例模式特点:

   1. 实现懒加载

   2. 并发下线程安全

   解析:

静态内部类单例模式如何解决懒加载的呢?

   首先了解一下一个类什么时候会立刻初始化呢?
     1.一个类的实例被创建
     2.类中声明的静态方法被调用
     3.类中声明的静态成员被赋值
     4.类中声明的静态成员被使用,并且这个成员不是常量成员
     5.类是顶级类,并且在类中有嵌套的断言语句

  由于没有直接new 对象,所以只会加载StaticInnerClassSingleton类,而不会进行初始化,进而实现了懒加载

 

静态内部类单例模式如何解决线程安全问题的呢?

    在类的初始化的期间,JVM会去获取一个锁(class对象初始化锁),这个锁可以同步多个线程对一个类的初始化 。
    也就是JVM会避免多个线程去初始化同一个类,基于jvm的这个特性
    在StaticInnerClassSingleton这个类中:    
    有会存在两个线程:
      0.InnerClass线程
      1.StaticInnerClassSingleton线程
     当线程0去初始化instance时,会获得 jvm的类加载的  class初始化锁,获取锁以后
     线程0的初始化操作对于线程1来说是不可以见的(‘非构造线程是不允许看到重排序的’)
     也就是说:instance = new StaticInnerClassSingleton();这条代码初始化instance时,

     不管是否重排序,对于线程1来说都是不可见的,所以 静态内部类可以解决  线程安全问题

 

5. 枚举单例模式

    基本写法:

   

/**
 * 枚举类 单例模式
 * @author jackcheng (jackcheng1117@163.com)
 */
public enum EnumSingleton {

    INSTANCE;

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

   枚举类单例模式特点:

    1. 实现懒加载

    2. 线程安全

枚举单例模式是比较完美的一种方式,在effictive java书中,作者也推荐使用这种方式。

 

通过jad反编译EnumSingleton,得到枚举类的构造器:

  private EnumSingleton(String s, int i) {
      super(s, i);
   }

 

 当我们试图通过利用反射获取EnumSingleton(String s, int i)这个构造器,发现会报错,说明反射是攻击不了的。

 

    其他单例模式的实现方式不再列举(HashMap形式、ThreadLocal形式等)

 

6.避免克隆单例模式实例

 

/**
 * 演示单例模式克隆案例
 * @author jackcheng (jackcheng1117@163.com)
 */
public class Singleton implements Cloneable {

    @Override
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	private Singleton() {
        if (instance != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    private static Singleton instance = null;

    private static boolean flag = false;

    public static Singleton getInstance() {
        if (instance == null) {
            flag = true;
            instance = new Singleton();
        }
        return instance;
    }

    static class Test {
        public static void main(String[] args) throws CloneNotSupportedException {
            Singleton instance = Singleton.getInstance();
            Singleton cloneInstance = (Singleton) instance.clone();

            System.out.println(instance);  //com.ssm.test.Singleton@5a42bbf4
            System.out.println(cloneInstance);  //com.ssm.test.Singleton@270421f5
            System.out.println(instance == cloneInstance);  //false
        }
    }
}

     当重写clone()方法后,克隆一份单例模式实例,测试发现,克隆的对象与单例实例竟然不是同一个对象

     很显然违背了单例模式的初衷,如何避免呢?

     将重写clone()方法改为 :

 @Override
 protected Object clone() throws CloneNotSupportedException {
     return getInstance();
 }

7. 避免序列化与反序列化造成不是同一个对象问题

    将单例类实现序列化接口,然后对单例实例进行序列化操作,然后再进行反序列化操作。

    发现序列化与反序列化之后竟然不是同一个对象,这不是也违背了单例模式的初衷吗?

    为什么呢?

        通过看源码发现,当进行反序列化时,会调用  ObjectInputStream.readObject()方法。

    ObjectInputStream.readObject()是通过反射来反序列化对象的,既然是经过反射,那么肯定或获取一个新的
     对象,即使对象所属类的构造器是private的,那么也是可以通过反射的newInstance方法来 new 一个对象
     当我们将HungrySingleton对象序列化再反序列出来时,由于反序列化时通过反射获取对象那么肯定会

      再new出来一个对象,自然和序列化时的对象是不一样的。

    如何解决呢?

    经过查看  ObjectInputStream.readObject()过程的源码,在进行反射获取对象时,会有一个判断,
   这个判断就是判断要反序列化的类中有没有Object readResolve()方法
   若有:则返回这个类的实例化对象(singleton)
   若没有:则通过反射的newInstance()方法new一个实例,然后将这个实例返回

    所以只需要在Singleton类中加一个方法即可:

 private Object readResolve() {
     return getInstance();
 }

 

 

总结:

  在使用单例模式时要注意以下几点:

   1. 线程是否安全

   2. 避免使用反射创建出对象

   3. 避免克隆及克隆对象不一致问题

   4. 避免序列化、反序列化时对象不一致问题

 

有兴趣的同学可以持续关注,或者有感兴趣的话题可以抛出来,一起探讨。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值