单例模式

一、何为单例

保证一个类只有一个实例,哪怕是在多线程同时访问的情境下,并需要提供一个全局访问此实例的点。

  1. 注意:
  1. 单例类只能有一个实例
  2. 这个实例由自己创建
  3. 这个实例必须提供给外界
  1. 关键:构造器私有化,声明静态方法实现全局访问获得该单例实例

  2. 创建方法:单例模式按个人理解是有8种方式实现的,但主要可以分为支持懒汉式和非懒汉式实现。
    饿汉式:在类的加载的时候就创建;
    懒汉式:只有在我需要的时候才去创建;

二、思路及实现

(核心就是:构造器私有化,声明静态方法实现全局访问获得该单例实例)

8种实现方式

  1. 静态类实现
  2. 懒汉模式(线程不安全)
  3. 懒汉模式(线程安全)
  4. 饿汉模式(线程安全)
  5. 使⽤类的静态内部类(线程安全)
  6. 双重锁校验(DEL)
  7. CAS「AtomicReference」(线程安全)
  8. Effective Java作者推荐的枚举单例(线程安全)

三、具体实现方法

1.静态类实现:

/**
*静态类的方式可以在第一次运行的时候直接初始化Map类,同时这里也不想要到延迟加载在使用
**/
public class Singleton {
public static Map<String,String> cache = new ConcurrentHashMap<String,
String>();
}

2.懒汉模式(线程不安全)

/*
*单例模式的一个特点就是不允许外部直接创建,所以这里在构造函数上加了private,但这种方法,在多线程的情况下,会造成多个单例同时存在,破坏了单例模式
*/
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance(){
if (null != instance) return instance;
instance = new Singleton();
return instance;
}
}

3.懒汉模式(线程安全)

/**
*该模式虽然安全,但是加锁之后,会占据资源导致浪费,一般不推荐
*/
public class Singleton_02 {
private static Singleton_02 instance;
private Singleton_02() {
}
public static synchronized Singleton_02 getInstance(){
if (null != instance) return instance;
instance = new Singleton_02();
return instance;
}

4.饿汉模式(线程安全)

/**
*这种程序一运行就加载的模式是安全的,但是,这好比你打开游戏,就给你加载全部地图,这会导致资源消耗严重
*/
public class Singleton_03 {
private static Singleton_03 instance = new Singleton_03();
private Singleton_03() {
}
public static Singleton_03 getInstance() {
return instance;
}
}

5.使⽤类的静态内部类(线程安全)

/*
*这种模式保证了线程安全,又保证了懒加载,同时又不会因为加锁的方式耗费性能,
因为jvm可以保证多线程并发访问的准确性,也就是一个类的构造方法在多线程环境下可以被正确加载,
是非常推荐的一种单例模式。
*/
public class Singleton_04 {
private static class SingletonHolder {
private static Singleton_04 instance = new Singleton_04();
}
private Singleton_04() {
}
public static Singleton_04 getInstance() {
return SingletonHolder.instance;
}
}

6.双重锁校验(DEL)
重点强调一点:在极端情况下,也是会出问题,因为instance = new Singleton_05();这一句是非原子性操作。

/*
*双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时,也满足了懒加载
*/
public class Singleton_05 {
private static Singleton_05 instance;
private Singleton_05() {
}
public static Singleton_05 getInstance(){
if(null != instance) return instance;
synchronized (Singleton_05.class){
if (null == instance){
instance = new Singleton_05();
 /*
    * 1.分配内存空间
    * 2、执行构造方法,初始化对象
    * 3、把这个对象指向者个空间
    *
    * 123
    * 指令重排之后:132 
    * A按132执行,当执行完3时,B线程进来,按123执行,因为已经分配了空间,但instance还没有
    * 完成构造,就发生错误了,所以要将 private static Singleton_05 instance;
    * 改为private volatile static Singleton_05 instance;
    * 亦或者,在getInstance上再加锁
    * */
}
}
return instance;
}
}

7.CAS「AtomicReference」(线程安全)

/*
*AtomicReference类提供了可以原子读取和写入的底层对象引用的操作,还包含高级原子操作。这也就解决了上面DCL的问题
*/
public class Singleton_06 {
private static final AtomicReference<Singleton_06> INSTANCE = new
AtomicReference<Singleton_06>();
private static Singleton_06 instance;
private Singleton_06() {
}
public static final Singleton_06 getInstance() {
for (; ; ) {
Singleton_06 instance = INSTANCE.get();
if (null != instance) return instance;
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}
public static void main(String[] args) {
System.out.println(Singleton_06.getInstance()); 
//org.itstack.demo.design.Singleton_06@2b193f2d
System.out.println(Singleton_06.getInstance()); 
//org.itstack.demo.design.Singleton_06@2b193f2d
}
}

睁大眼睛:重点来了!!java语言实现动态化的灵魂——反射!!通过反射,得到setAccessible,设置为setAccessible(true),然后就完了,上面这些方式创建的单例,全部都被破坏了。

四、破坏单例模式

public class DCLSingle {
    private static DCLSingle single;
    private DCLSingle(){
    }
    public static DCLSingle getInstance(){
        //第一次判断,没有这个对象才加锁
        if(single == null){
            //哪个需要保护,就锁哪个
            synchronized (DCLSingle.class){
                //第二次判断,没有就实例化
                if(single == null){
                    single = new DCLSingle();
                }
            }
        }
        return single;
    }
    
    //通过反射破化单例
    public static void main(String[] args) throws Exception {
        LazySingle single = LazySingle.getInstance();
        Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingle single1 = constructor.newInstance();
        System.out.println(single == single1);//false
    }
}
//得到单例类的构造器,然后通过newInstance的方法创建对象,很明显破化了单例

【改进代码,防止搞破化】

既然这次你是通过得到构造器破化的,那我给构造器加个方法,如果你已经创建了实例,那就抛出异常

private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null){
            throw new RuntimeException("破坏失败");
        }
    }
}

但是这个又有问题,这里的判断是private static DCLSingle single 是否有值,如果我们都不通过getInstance()方法创建对象,而是这样

public static void main(String[] args) throws Exception {
 //   LazySingle single = LazySingle.getInstance();
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    //注意:这里的对象不是单例类中里面属性的那个对象
    LazySingle single = constructor.newInstance();
    LazySingle single1 = constructor.newInstance();
    System.out.println(single == single1);//false
}
//这里根本不会抛出异常,而是又破坏了单例

【继续改进代码,防止搞破化】

可以利用红路灯原理,防止破化

//加个标志
private static String sign = "password";
private LazySingle(){
    synchronized(LazySingle.class){
        if(single!=null || !"password".equals(sign)){
            throw new RuntimeException("破坏失败");
        }else{
            sign = "no";
        }
    } 
}

此刻你通过上述main()方法里面的内容测试,发现又会抛出异常。然而我们能通过反射获得构造方法,那我们同样也能通过反射获取对象的属性以及值吧
【再度破化】

public static void main(String[] args) throws Exception {
    Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Field field = LazySingle.class.getDeclaredField("sign");
    //此处省略通过反射获取该属性的类型和方法....
    LazySingle single1 = constructor.newInstance();
    //重新变回原标志位
    field.set("sign","password");
    LazySingle single2 = constructor.newInstance();
    System.out.println(single2 == single1);//false
}

所以最终的解决方法就是第八种方法

五、使用Effective Java作者推荐的枚举单例(线程安全),防止单例被破坏

枚举:通过反射破解枚举发现不成功:

  1. 普通的反编译会欺骗开发者,说enum枚举是无参构造
  2. 实际enum为有参构造(见后面)
  3. 通过反射破解枚举会发现抛出异常
    Exception in thread “main” java.lang.IllegalArgumentException: Cannot
    reflectively create enum objects at
    java.lang.reflect.Constructor.newInstance(Constructor.java:417)

通过idea和jdk自带的反编译枚举如下:
在这里插入图片描述
通过jad反编译枚举的代码如下
在这里插入图片描述
枚举是有参构造!
方式:

public enum Singleton_07 {
INSTANCE;
public void test(){
System.out.println("hi~");
}
}
//使用
@Test
public void test() {
Singleton_07.INSTANCE.test();

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。但要注意的是:此方式在继承场景下不可用。
参考文章1
参考文章2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值