单例你写对了吗?你的单例可以被破坏吗?

关于单例的描述:在一个系统中,有且只有一个实例。

你的单例真的只生产一个实例吗?有办法破坏单例吗?

下面我们看看单例的几种写法,以及单实例是怎么被破坏的。

单例写法(一)、饿汉式单例

public class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    private Singleton() {

    }

    public static Singleton getInstance() {
        return SINGLETON;
    }
}

第一种饿汉式很简单,一般也是用的比较多的一种,虽说比较占用资源,现在内存什么的都很便宜了,就没必要计较这一点开销。如果在犹豫用什么方式去写,那么就用饿汉式吧。 但这种单例可以被反射破坏。我们知道,把构造方法私有化可以防止new的方式创建实例,但是日防夜防,却防不住反射。

只要调用一下newInstance方法就又可以产生新的实例了

Singleton.class.newInstance()

单例写法(二)、方法锁

public class Singleton {
    private static Singleton instance;
    private Singleton() {

    }

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

第二种方法锁是懒汉式的单例写法,但是非常影响性能。一般不推荐使用。通过反射同样可以破坏单实例。

单例写法(三)、代码块锁,双重判断

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {

    }

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

第三种代码块锁的方式,一定要加上双重判断。并且要结合volatile关键字一起使用。volatile关键词使instance变量在多线程之间可见,解决了JVM重排序问题。

我们以为的对象的初始化顺序应该是这样的:

但它有可能是这样的:

java new操作是不具备原子性(原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。)的,也就是说第二步,第三步顺序是不能保证的,有可能因为重排序问题被调整位置。 当第三步,出现在第二步时,已经分配了内存地址,那么对象就不是null,可以调用了,但是由于还没有调用构造方法初始化,又会报错。因此需要用volatile关键词来禁止重排序。

而双重if判断也是至关重要的。试想,当线程A访问getInstance方法,instance为null,进入了synchronized。这时线程B又进来了,由于线程A还没有new完这个对象, 因此线程B看到的instance依然是null,接着又进入了synchronized代码块,然后去new一个实例。这样线程A和线程B得到就不是同一个实例了,因此也无法达到单例的效果。

第三种方式也是无法避免反射来创建多实例的。

单例写法(四)、静态内部类方式

public class Singleton {
    private static class Instance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Instance.INSTANCE;
    }
}

第四种静态内部类的方式也是一种懒汉式的写法。但依然没法防住反射创建多个实例。

单例写法(五)、枚举方式

public enum  Singleton {
    INSTANCE;

    public void method() {
        
    }
}

第五种是推荐使用的一种方法,用枚举来实现单例,枚举可以避免反射构建新实例。当你对枚举进行newInstance调用的时候会出现以下错误:

Exception in thread "main" java.lang.InstantiationException: Singleton
	at java.base/java.lang.Class.newInstance(Class.java:571)
	at Test.main(Test.java:3)
Caused by: java.lang.NoSuchMethodException: Singleton.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3349)
	at java.base/java.lang.Class.newInstance(Class.java:556)
	... 1 more

可能看到这么多单例的写法有些眼花缭乱,越来越糊涂了。其实,在日常的开发中,任何一种都可以。只要注意别反射调用自己写的单例就好了。毕竟在java开发中有约定大于配置的说明。约定好就可以了,做到心里有数。

本博客采用 知识共享署名-禁止演绎 4.0 国际许可协议 进行许可

本文标题:单例你写对了吗?你的单例可以被破坏吗?

本文地址:https://dev-tang.com/post/2020/03/singleton.html

推荐阅读