[JUC-16] 单例模式

1、饿汉式

  实现:

public class 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 Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }
}

  存在的问题:如果实例从头到尾就没用过,而这个实例对象又占用了较大内存,容易造成内存泄漏;

  改进:改成需要使用实例对象时再创建。

2、饱汉式(懒加载)

  最简单的实现:

public class LazyMan {

    private LazyMan() {
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

  对于单线程,这种实现毫无问题,但是对于多线程来说,如果线程 A 判断完 if 但是还没来得及执行lazyMan = new LazyMan();,此时线程 B 获取 CPU 资源,也来到 if ,也判断为 null,并创建一个实例对象进行返回。接着线程 A 苏醒,又执行了一次创建对象。

  测试代码:

public class Test{
    public static void main(String[] args) throws InterruptedException {
        Thread A = new Thread(()->{
                LazyMan.getInstance();
        });
        Thread B = new Thread(()->{
            LazyMan.getInstance();
        });
        A.start();
        B.start();
    }
}

  运行结果:
在这里插入图片描述
  加个延时:

public class Test{
	public static void main(String[] args) throws InterruptedException {
        Thread A = new Thread(()->{
                LazyMan.getInstance();
        });
        Thread B = new Thread(()->{
            LazyMan.getInstance();
        });
        A.start();
        TimeUnit.SECONDS.sleep(1);
        B.start();
    }
}

  运行结果:
在这里插入图片描述
  可见,多线程下,这种方式的单例模式是有问题的。

  解决方式:使用双重检测锁。

3、DCL懒加载

  DCL:Double Check Lock。

  代码实现:

public class LazyMan {

    private LazyMan() {
        System.out.println("LazyMan 创建了");
    }

    private static LazyMan lazyMan;

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

  无论多少线程,都可以发现只创建了一个实例。

  你可能会问:为什么不干脆在getInstance方法直接加synchronized关键字呢?这是因为,getInstance方法调用是很频繁的,假设已经有了实例,多个线程来获取实例时,总是在判断锁,导致多个线程同时要拿实例时,变得要阻塞了。

  这种 DCL 方式是否一定 OK 呢?答案是 :No!

  我们学习 volatile 时了解了指令重排的问题。对于语句lazyMan = new LazyMan();我们看起来以为只是一行,其实它底层有三步:
  1、分配内存空间
  2、执行构造方法,初始化对象
  3、把这个对象指向这个空间

  如果是正常的123顺序,那么就没问题。如果来个指令重排,变成了132,就容易出现问题了。

  假设指令重排变成了132。线程 A 进入lazyMan = new LazyMan();语句的3,即对象已经分配了空间,并且引用也指向了这个对象,但是还没有执行构造函数初始化对象。这时,线程 A 暂停,线程 B 进入了获取实例的方法,线程 B 判断出来的是对象不为空,于是线程 B 拿到了一个没有初始化的对象。

  解决方法就是对实例对象加 volatile 关键字。于是,完整的DCL懒加载如下所示:

public class LazyMan {

    private LazyMan() {
        System.out.println("LazyMan 创建了");
    }

    private volatile static LazyMan lazyMan;

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

4、使用枚举实现单例模式

  正所谓道高一尺魔高一丈,我们知道 Java 是有反射机制的,如果有人这样操作:

public class Test{
    public static void main(String[] args) throws Exception {
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance1 = constructor.newInstance();
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

  运行结果:
在这里插入图片描述
  此时发现,DCL的单例模式被反射给破坏了。

  解决方法:在构造器中使用标志变量。

public class LazyMan {

    private static boolean flag = false;

    private LazyMan() {
        synchronized (LazyMan.class) {
            if (flag){
                throw new RuntimeException("我知道已经有了实例你还在创建实例,不要试图使用反射破坏异常");
            }else {
                System.out.println("LazyMan 创建了");
                flag = true;
            }
        }
    }

    private static LazyMan lazyMan;

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

  测试代码:

public class Test{
    public static void main(String[] args) throws Exception {
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance1 = constructor.newInstance();
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

  运行结果:
在这里插入图片描述
  可见,有了实例对象的前提下,如果再调用构造函数,我们就可以知道它是打算破坏单例模式,但是被我们的代码给遏制了。

  但是,这样就完全安全了吗?答案还是:No!

  如果有人这样做:

public class Test{
    public static void main(String[] args) throws Exception {
        Field flag = LazyMan.class.getDeclaredField("flag");
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance1 = constructor.newInstance();
        flag.set(instance1,false);
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

  运行结果:
在这里插入图片描述
  也就是说,创建了一个实例对象后,本来 flag 已经变成 true 了,可是被反射获取后又强行改成了 false,导致还能再成功调用一次构造器。

  我靠,这样搞下去,没法玩了。

  如果不是被人为反射搞破坏,其实DCL懒加载已经够用,既然能被反射搞成这样,那我就从反射本身去想办法。

  查看反射获取的构造器进行创建实例对象的源码:
在这里插入图片描述
  终于找到敌人的弱点了,反射不能搞枚举类型,哈哈哈哈。

  使用枚举实现单例模式:

public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

  使用反射来获取一下:

class Test{
    public static void main(String[] args)throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getConstructor();
        constructor.setAccessible(true);
        EnumSingle instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

  运行结果:
在这里插入图片描述
  不对啊,不是应该说枚举不能被反射么,这里的异常显示是找不到构造函数??

  查看一下这个枚举类型的反编译结果:
在这里插入图片描述
  这不是有空参构造么?

  换一个反编译工具 jad ,对 EnumSingle.class 进行反编译。

jad -sjava EnumSingle.class

  打开反编译的结果:
在这里插入图片描述
  首先可以确定它确实是一个 class,并且继承了 Enum。

  它并没有空参构造,只有一个有参构造private EnumSingle(String s, int i),那我们就从这里突破:

class Test {
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

  运行结果:
在这里插入图片描述
  说明枚举类型真的不能被反射进行创建实例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值