玩转单例模式

单例模式,就是一个类只能有一个实例。

单例模式的关键在于构造器私有化,这样就不能在外面new。


Eager Mode

/**
 * eager mode of singleton
 * load the instance as soon as the class file is loaded by classloader
 */
public class EagerMode {

    private static EagerMode eagerMode = new EagerMode();

    private EagerMode(){}

    public static EagerMode getEagerMode(){
        return eagerMode;
    }

}

类被加载时,实例就会创建,这叫饿汉模式。

对于轻量级的对象,倒是可以这么做。

如果从头到尾都没有用到这个对象,那么又是一种对资源的浪费。

那么,我们就懒加载吧。

Lazy Mode

懒加载就是,你要实例的时候我才new出来,不是你要不要我都new。

package singletonPattern;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        if(lazyMode == null){
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

代码本身没有问题,但是它经不住多线程的压力。

我在getLazyMode里面打印一句话:

package singletonPattern;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

多个线程的情况,如果是安全的话,那么打印只会执行一次:

为了加强效果,我让所有到达getLazyMode入口的线程都睡300毫秒:

package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

测试方法:

@Test
    public void test02() {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{

                LazyMode.getLazyMode();
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

结果:

the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null



每打印一句the instance is null,就会new一个对象,所以,单例模式就被破坏了。

解决的办法是:加锁。


Thread-safe Lazy Mode

package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public synchronized static LazyMode getLazyMode(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

这铁定是没有问题的,但是,100个线程先后在getLazyMode这里排队,太慢了。

问题的关键是,真正走进if(lazyMode == null)的只有一个线程,其他的直接return实例就行了。

因此我们可以用synchronize代码块:

package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            synchronized (LazyMode.class){
                System.out.println("create the instance!");
                lazyMode = new LazyMode();
            }
        }

        return lazyMode;
    }
}

如果lazyMode是null,那就去new,否则的话,你就直接return。

这比在方法上加synchronize效率高多了。

但是,如果有两个线程同时走到if(lazyMode == null),同样会有线程安全问题。经过测试也测到了打印两句create the instance!的情况。

于是,我们还要加一层判断:

 public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //to make the code efficient
        if(lazyMode == null){
            synchronized (LazyMode.class){
                //to make thread safe
                if(lazyMode == null){
                    System.out.println("create the instance!");
                    lazyMode = new LazyMode();
                }
            }
        }

        return lazyMode;
    }

这叫Double Check Lock,双重判空。

第一层判空,是为了让代码更有效、更快。

第二层判空,是为了线程安全。想象一下,如果有两个线程进了if(lazyMode == null),其中一个线程先进synchronize块,使得lazyMode有值,这样第二个线程就进不了第二层的if(lazyMode == null)

这似乎已经完美了,但还有一个问题,那就是new LazyMode()并非原子性的。

原子性,就是一个单一操作,new一个对象,有三个操作:

1.给LazyMode实例分配内存空间
2.执行LazyMode的构造方法
3.使变量lazyMode指向堆中的LazyMode对象。

因为JVM有指令重排,所以最后的顺序可能不是1-2-3,而是1-3-2。

假设线程A走了1-3,这时lazyMode已经不是null了,所以线程B会直接return,这就出错了。

虽然概率小,但是我们还是要想办法避免。

package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static volatile LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //to make the code efficient
        if(lazyMode == null){
            synchronized (LazyMode.class){
                //to make thread safe
                if(lazyMode == null){
                    System.out.println("create the instance!");
                    //not atomic
                    lazyMode = new LazyMode();
                }
            }
        }

        return lazyMode;
    }
}

lazyMode加上volatile修饰,就会防止指令重排,读操作一定后于写操作,所以线程B拿到的对象没有问题。

至此,线程安全的、懒加载的单例模式就成型了。

Static Inner Class

还有一种写法,就是静态内部类的写法:

package singletonPattern;

/**
 * we use static inner class to restore the instance
 * 
 */
public class Outer {
    private Outer(){

    }

    private static class Inner{
        private static final Outer INSTANCE = new Outer();
    }

    public static Outer getInstance(){
        return Inner.INSTANCE;
    }
}

这对比饿汉模式的好处是,它是懒加载的。

只有调用Inner.INSTANCE时,才会去new对象。关于这一点,debug一下就能看到。

同时,它又是线程安全的。JVM在类的初始化阶段会获取一个锁,这个锁将保证完成实例化的线程只有一个。

因此,这也是很棒的写法。

destroy everything by reflection

上面的东西好像很厉害,但是,反射技术击破一切。

拿eager mode开刀吧(其他都一样的):

  @Test
    public void test04() throws Exception {
        Constructor<EagerMode> declaredConstructor = EagerMode.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EagerMode instance1 = declaredConstructor.newInstance();
        EagerMode instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

只要用declaredConstructor.setAccessible(true);,构造器的private就不起作用了。结果是:

singletonPattern.EagerMode@5b80350b
singletonPattern.EagerMode@5d6f64b1

两个实例。

不过我们可以在构造器上加异常来防止反射。

  private EagerMode(){
        if(eagerMode != null){
            throw new RuntimeException("Don't use reflection, stupid ass!");
        }
    }

但是,对于LazyMode,这一招并不管用,由于是懒加载,实例一开始确实是null,于是就不会抛出异常。

那么,怎么做才能对反射免疫呢?


Enum

我们看到,在newInstance方法里:

  @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

如果构造器存在于枚举类的话,直接报错。

我们试一下:

枚举类的单例模式:

package singletonPattern;

public enum Singleton {
    INSTANCE;
}

搞它:

 @Test
    public void test06() throws Exception{
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println(singleton);

    }

报的错是:

java.lang.NoSuchMethodException: singletonPattern.Singleton.<init>()

是构造方法调的不对。

我们最好能看到枚举类反编译的结果:

使用jad反编译Singleton.class:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   Singleton.java

package singletonPattern;


public final class Singleton extends Enum
{

    public static Singleton[] values()
    {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String name)
    {
        return (Singleton)Enum.valueOf(singletonPattern/Singleton, name);
    }

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

    public static final Singleton INSTANCE;
    private static final Singleton $VALUES[];

    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

构造器里面还需要两个参数:

    @Test
    public void test06() throws Exception{
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println(singleton);

    }

这时候报的错就是我们预料的了:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

至于为什么需要两个参数,因为每个枚举类都继承了Enum:

protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

使用super调用了父类构造器。


单例模式最终使用枚举类的方式最强悍!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值