单例模式详解


饿汉式

/**
 * 饿汉模式(静态变量)
 */
public class HungryMan {
	//构造器私有化 (防止 new )
    private HungryMan(){}
	
    //类的内部创建对象
    private final static HungryMan hungryMan = new HungryMan();
    //对外暴露一个静态的公共方法
    public static HungryMan getInstance(){
        return hungryMan;
    }
}

/**
 * 饿汉模式(静态代码块)
 */
public class HungryMan {
   
	//构造器私有化 (防止 new )
    private Hungry(){}
	
    //类的内部创建对象
    private final static HungryMan hungryMan;
    
    static{
        hungryMan = new HungryMan();
    }
	
    //对外暴露一个静态的公共方法
    public static HungryMan getInstance(){
        return hungryMan;
    }
}

以上的两种方式其实差不多,有一样的优缺点:

  1. 在类加载时完成实例化,避免了线程同步问题。

  2. 如果从头到尾都没有用过该实例,就会造成内存的浪费。

    总结:可以用,但可能会造成浪费,不推荐使用


懒汉式


/**
* 懒汉式(线程不安全)
*/
public class LazyMan {
    private LazyMan(){}

    private static LazyMan instance;

    //提供一个静态的公有方法,当使用到该方法时,再去创建instance
    public static LazyMan getInstance(){
        if(instance == null){
            instance = new LazyMan();
        }
        return instance;
    }

}

虽然起到了懒加载的效果,避免内存浪费。但是会造成线程不安全。

如果在多线程下,一个线程进入了if判空,还未来得及往下new对象,另一线程也进入了If判空,就会产生多个实例。因此,在实际开发中不能这样使用单例模式。

所以我们就会想到要解决线程不安全的问题,在getInstnce()方法中加上 synchronized锁

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

但是这样会造成效率低下的问题,每个线程在想获得类的实例时,还得等其他线程执行完这个同步方法。实际上,这个方法只需要执行一次实例化代码就够了,后面的直接return。

有一种很优秀的DCL(双重检查锁)模式,不仅达到了懒加载效果,还保证了线程安全以及提高效率。

//双重检查代码(Double-Check)
public class LazyMan {
    //单例模式必须构造器私有
    private LazyMan(){}

    private static LazyMan lazyMan;

    //双重检测锁模式  懒汉式单例  DCL(双重检查锁)懒汉式
    public static LazyMan getInstance(){
        //第一层判空 提高性能 避免资源浪费
        //如果不判空的话  就跟第三种的效率是一样低的
        if ( lazyMan == null ) {   //step1
            synchronized (LazyMan.class) {
                //由于可能多个线程都进入了step1,由于锁定机制,一个线程进入该代码块时,其他线程
                //仍在排队进入该代码块,如果不做判断,当前线程即使创造了实例,下一个线程也不知道,就会继续创建一个实例。
                if (lazyMan == null) {
                    lazyMan = new LazyMan();  //不是一个原子性操作
                }
            }
        }
        return lazyMan;
    }

    //我们可以创建一个man方法进行测试
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan lazyMan = LazyMan.getInstance();
                System.out.println(lazyMan.hashCode());
            }).start();
        }
    }
}

结果显示:
image-20201101101654567

看起来似乎没有什么问题了,但实际上存在一个并发陷阱---- 当lazyMan != null时,仍可能指向一个空对象

因为 lazyMan = new LazyMan(); //不是一个原子性操作,它可以被抽象为下面几条JVM指令

memory = allocate();	//1:分配对象的内存空间
initLazyMan(memory);	//2:初始化对象
lazyMan = memory;		//3:设置lazyMan指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序

操作 3很有可能 排在了操作 2 之前,即引用lazyMan指向内存memory时,这段崭新的内存还没有初始化——即,引用lazyMan指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于lazyMan已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。


这个时候,需要volatile来解决了! vlatile能保持内存可见性

//再静态变量前加上 volatile关键字
private static volatile LazyMan lazyMan;

静态内部类

//静态内部类  既达到了懒加载效果,又保证了线程安全
public class OuterClass {

    private OuterClass() {
    }

    public static class InnerClass {
        private static final OuterClass outerClass = new OuterClass();
    }

    public static OuterClass getInstance(){
        return InnerClass.outerClass;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                OuterClass outerClass = OuterClass.getInstance();
                System.out.println(outerClass.hashCode());
            }).start();
        }
    }
}

结果就不贴出来了,每个线程里的实例化对象哈希码都是一样,即同个实例。

当Holder类被加载时,静态内部类并不会马上被加载,只有调用Holder的getInstance()方法时,静态内部类才会被加载,这就实现了第一点:懒加载效果;

而类的静态属性只会在第一次加载类时初始化。 也就是说当JVM装载类时,底层提供了装载机制保证了初始化是单线程的,即线程安全的。

总结:推荐使用

枚举

//枚举本身也是一个class
public enum SingleTon {
    INSTANCE;
}

//创建一个测试类
public class TestSingleton {
    public static void main(String[] args) {
        SingleTon instance =  SingleTon.INSTANCE;
        SingleTon instance2 = SingleTon.INSTANCE;

        System.out.println(instance == instance2);
    }
}

结果输出:
image-20201101151725101


反射会破坏单例模式



    public static void main(String[] args) throws Exception {

        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);

        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //跟LazyMan.getInstance(); 不是同一个实例
        LazyMan instance = declaredConstructor.newInstance();

        flag.set(instance,false);

        //当设置有标志位,两个通过反射生成的实例对象依旧有办法通过 并且不是同一个实例
        //利用属性重新赋值  flag.set(instance,false);
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }


反射不能破坏枚举

import java.lang.reflect.Constructor;
import java.util.EmptyStackException;

//enum 是一个什么? 本身也是一个class类
public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        //EnumSingle instance2 = EnumSingle.INSTANCE;
		
        
        Constructor<EnumSingle>  declaredConstruct = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstruct.setAccessible(true);
       
        EnumSingle instance2 = declaredConstruct.newInstance();
			
        System.out.println(instance1);
        System.out.println(instance2);
        
         // java.lang.NoSuchMethodException: single.EnumSingle.<init>()   没有空参构造器  不是我们想要的错误
        //  把无参换成有参 就会报我们理想的错误:Cannot reflectively create enum objects
    }
}

总结

  1. 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需
    要频繁创建销毁的对象,使用单例模式可以提高系统性能。
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使
    用new。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值