设计模式-单例模式
4.24 修改一些对静态内部类的解释
在日常学习中,经常会碰到单例模式,所以在这里系统的记录一下单例模式。
首先,让我们看一下它的定义,Java中对单例模式的定义是:
一个类有且仅有一个实例,并且自行实例化向整个系统提供
所以我们需要实现的就是如何保证一个类有且仅有一个实例。
实现过程
为了不让其他类来创建这个对象,我们首先要做到的就是构造器私有。接着,我们直接在类中定义一个对象,即可实现唯一
public class Singleton{
//构造器私有
private Singleton(){
}
private final static Singleton singleton = new Singleton();
//让其他类获得
public static Singleton getInstance(){
return singleton;
}
}
这种创建实例的方法就是我们常见的饿汉式单例。其名字是由the singleton instance is early created at complie time中的early音译过来的。
显而易见,饿汉式单例在类加载时会将类中方法,属性直接加载完成。当我们还未使用它时,它便自动加载了,这样难免会造成内存的消耗。
为了解决上诉问题,所以就有了第二种的单例类型:懒汉式单例。其名字是由the singleton instance is lazily created at runtime中的lazily意译过来的。
让我们看下是如何创建的。
public class Singleton{
//构造器私有
private Singleton(){
}
private static Singleton singleton;
//当需要使用的时候才加载 符合名字
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
当我们使用以上代码时,在一定条件下,确实可以保证单例。这个条件便是单线程!该方法在并发下,便不能保证单例,所以我们要对该方法做一定的改进。
讲到并发,我们第一个想到的肯定就是锁,那么我们开始进行第一次改造:
private static Singleton singleton;
//将synchronize加到类上
public synchronized static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
将方法加到类上,是可行的,但是锁的粒度太大了,会影响整个效率,那么我们减小锁的粒度,对其进行第二次改造:
private static Singleton singleton;
public static Singleton getInstance(){
//给对象进行加锁
//判断两次是为了,两个对象可能在第一次判空后才开始进行锁竞争 若加在最外层 则与上一种情况类似
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
利用双重检测来保证了线程上的安全,这个就是懒汉-DCL的模式。从线程的角度来讲,现在已经能够保证只取到一个对象了。但是由于singleton = new Singleton()这行代码的原因,还会产生一种错误:线程会拿到还未初始化完成的对象。因为这句代码不具备原子性,在JVM层面上,会进行指令上的重排序。为了避免指令上的重排序,所以我们要用volatile进行带三次改造。
private volatile static Singleton singleton;
public static Singleton getInstance(){
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
至此,我们好像得到了很优秀的一种单例模式。但是这些就是完美的嘛?不是的,还有反射这个拦路虎。
我们对进行如下尝试:
Singleton singleton1 = Singleton.getInstance();
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1);
System.out.println(singleton2);
发现输出了:
很明显,单例模式被破解了。那么我们还需继续进行改进,在构造器上再加上一步判断
private Singleton() {
synchronized(Singleton.class){
if (singleton != null){
throw new RuntimeException();
}
}
}
上述问题被解决了。
但是若我们都用反射创建对象会如何呢?
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1);
System.out.println(singleton2);
依然出现了问题。此时我们需要利用一个变量来记录,当第一次被调用时,改变状态,当第二次被调用时直接返回异常。
private static boolean jathow = false;
private Singleton() {
synchronized(Singleton.class){
if (jathow == false){
jathow = true;
}else {
throw new RuntimeException("禁止反射");
}
}
}
但是对于上述改进,仍有破解办法。
Field jathow = Singleton.class.getDeclaredField("jathow");
jathow.setAccessible(true);
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
jathow.set(singleton1, false);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1);
System.out.println(singleton2);
我们对可以对属性进行加密,增大破解难度,但是没有从根源上解决这个问题。
我们只能从反射的源码进行分析,看看有什么办法可以从根源上解决这个问题。查看源码我们可以看到
所以枚举类单例就是我们最后的救星了。
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
至此算是解决了我目前这个层面能想到的问题。
还有一种算是饿汉模式升级版的静态内部类。
由于是内部类 所以在外面类进行加载时 不会第一时间加载类内部的实例,也就实现了延迟加载。
那么它是如何保证线程安全的呢?
由于JVM的类加载机制存在的
那么DCL与它的差异在哪里呢?
差异在于 DCL模式可以自己传入参数 ,对于实例对象做一点小小的定制。但是静态内部类加载则不行了,所以从这个角度来看不够灵活。
具体在工作中如何使用就看个人选择了。
- 静态内部类
public class Singleton {
private Singleton() {
}
public static Singleton getInstance(){
return InnerClass.SINGLETON;
}
private static class InnerClass{
private static final Singleton SINGLETON = new Singleton();
}
}