Java-单例模式

什么是单例模式?

  单例对象的类必须保证只有一个实例存在;

单例模式要考虑的几个方面?

  线程安全,延迟加载,序列化与反序列化安全

几种实现方式:

第一种:简单的懒汉或恶汉模式

/**
 * 单例模式
 */
public class SingletonTest {

    private static SingletonTest singletonTest = null;

    private SingletonTest() {

    }

    public static SingletonTest instance() {
        if (singletonTest == null) {
            singletonTest = new SingletonTest();
        }
        return singletonTest;
    }
}

第二种:方法上添加synchronized关键字,可以实现线程安全,但由于锁加在了方法上,性能较低;

/**
 * 单例模式
 */
public class SingletonTest {

    private static SingletonTest singletonTest = null;

    private SingletonTest() {

    }

    public static synchronized SingletonTest instance() {
        if (singletonTest == null) {
            singletonTest = new SingletonTest();
        }
        return singletonTest;
    }
}

第三种:双重检测机制

  双重检测机制,主要是为了防止一个线程进入锁之后,另一个线程也已经过了instance==null的校验之后出现的问题;

/**
 * 单例模式
 */
public class SingletonTest {

    private static SingletonTest singletonTest = null;

    private SingletonTest() {

    }

    public static SingletonTest instance() {
        if (singletonTest == null) {
            synchronized (SingletonTest.class) {
                if (singletonTest == null) {
                    singletonTest = new SingletonTest();
                }
            }

        }
        return singletonTest;
    }
}

第四种:valatile机制

首先要明白两个点:原子操作,指令重排;

1. 原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。

比如,简单的赋值是一个原子操作:m = 6; 假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而声明并赋值就不是一个原子操作:int n = 6; 对于这个语句,至少有两个操作:

①声明一个变量n
②给n赋值为6

——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。

——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

2. 指令重排:

  指令重排:Java中,是JVM为了提交执行效率做的一些优化,即在不影响结果的情况下,可以能会对一些语句的执行顺序进行调整;

int a ; // 语句1 
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4

  正常来说,对于顺序结构,执行的顺序是自上到下,也即1234;但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
  由于语句3和4不是原子操作,所以语句3和语句4也可能会拆分成原子操作,再重排。也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

而在单例中,由于singleton = new Singleton()这句并非是一个原子操作,所以会被编译器编译成如下JVM指令:
a. 给对象singleton分配内存空间 :memory=allocate();
b. 调用构造函数初始化对象singleton:ctorInstance(memory);
c. 设置singleton 指向刚分配的内存空间(这一步执行完singleton 就不是null了);
但是由于JVM中存在指令重排的优化,上面第二步和第三步的执行顺序不是固定的,所以最终执行的顺序可能是abc,也可能是acb。如果是后者,则在c执行完,而b没执行之前,另一个线程2在读到第一个singleton ==null时,这时候singleton 已经不是null(但却没有初始化),所以线程2会返回singleton ,但该singleton 直接使用就会报错了;也就是说,存在一个【singleton 已经不是null但还没有初始化】的中间状态;
这里的关键在于线程1对singleton 的写操作还没完成,线程2就执行了读操作;

而要避免这种操作,可以通过关键字volatile关键字来实现;
volatile关键字的一个作用就是禁止指令重排,保证了指令的顺序执行,这样在线程2看来,singleton 对象的引用要么指向null,要么指向一个初始化完成的singleton ,而不会出现某个中间状态;

volatile的一个问题是反射,不过可以把类设置为抽象类;

/**
 * 单例模式
 */
public class SingletonTest {

    private static volatile SingletonTest singletonTest = null;

    private SingletonTest() {

    }

    public static SingletonTest instance() {
        if (singletonTest == null) {
            synchronized (SingletonTest.class) {
                if (singletonTest == null) {
                    singletonTest = new SingletonTest();
                }
            }

        }
        return singletonTest;
    }
}

第五种:Effective Java推荐,内部类的懒加载模式

这种写法非常巧妙:

  • 对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。

  • 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

  • 它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
/**
 * 单例模式
 */
public class SingletonTest {

    private static class SingletonHolder {
        private static final SingletonTest INSTANCE = new SingletonTest();
    }
    
    private SingletonTest() {
    }
    
    public static SingletonTest instance() {
        return SingletonHolder.INSTANCE;
    }
}

第六种:Effective Java推荐,枚举;

由于创建枚举实例的过程是线程安全的,所以枚举的写法也没有同步的问题;

public enum SingleInstance {

    INSTANCE;

    public void fun1() { 

        // do something

    }

}


// 使用

SingleInstance.INSTANCE.fun1();

参考自:https://www.cnblogs.com/dongyu666/p/6971783.html

公众号:《算法爱好者》

原文地址:https://www.cnblogs.com/xiaozhang2014/p/7912011.html