单例模式的核心,就是全局只有一个实例。下面就每一种创建方式分析其优缺点。
1.饿汉式
/**
* 饿汉式
*/
public class PersonHungry {
private static PersonHungry INSTANCE = new PersonHungry();
private PersonHungry() {}
public static PersonHungry getInstance() {
return INSTANCE;
}
}
饿汉式优点:饿汉式在类加载时,就已经创建了实例对象,故不存在线程安不安全的问题。
饿汉式缺点:类加载时就创建了实例对象,比如只是调用里面某个static方法,这个时候就会创建了该实例对象了,有可能一直都不需要getInstance,因此这样会造成资源浪费。
2.静态代码块
public class Person {
private static Person INSTANCE = null;
static {
INSTANCE = new Person();
}
private Person() {}
public static Person getInstance() {
return INSTANCE;
}
}
静态代码块其实相当于另外一种饿汉式,类加载的时候,对象就会创建,虽说线程安全,但是资源浪费。
3.懒汉式1和2
public class PersonLazy1 {
private static PersonLazy1 mInstance = null;
private PersonLazy1() {}
/**
* 该方式在多线程运行下,实例不唯一
*/
public static PersonLazy1 getInstance() {
if (mInstance == null) {
mInstance = new PersonLazy1();
}
return mInstance;
}
}
public class PersonLazy2 {
private static PersonLazy2 mInstance = null;
private PersonLazy2() {}
/**
* 耗性能
*/
public synchronized static PersonLazy2 getInstance() {
if (mInstance == null) {
mInstance = new PersonLazy2();
}
return mInstance;
}
}
懒汉式1,在多线程的情况下,没法保证只创建一个实例对象。所以演变出懒汉式2,这种方式从效果的角度是可以实现单例的,通过synchronized保证了多线程的安全性。但是,懒汉式2最大的缺点就是耗性能,不管是否已经创建了实例,每次调用getInstance都需要锁。
4.懒汉式3和4
public class PersonLazy3 {
private static PersonLazy3 mInstance = null;
private PersonLazy3() {}
/**
* 改进性能,双重判断
*/
public static PersonLazy3 getInstance() {
if (mInstance == null) {
synchronized (PersonLazy3.class) {
if (mInstance == null) {
mInstance = new PersonLazy3();
}
}
}
return mInstance;
}
}
/**
* 推荐使用的方式
*/
public class PersonLazy4 {
// 加上volatile关键字,最值得推荐的方式
private static volatile PersonLazy4 mInstance = null;
private PersonLazy4() {}
/**
* 改进性能,双重判断
*/
public static PersonLazy4 getInstance() {
if (mInstance == null) {
synchronized (PersonLazy4.class) {
if (mInstance == null) {
mInstance = new PersonLazy4();
}
}
}
return mInstance;
}
}
懒汉式3和4都是使用双重判断再加锁的形式,即DCL。相比于懒汉式2,改进了性能,只有在第一次getInstance时会比较耗性能。而4比3多了一个关键字叫volatile。那么volatile关键字有什么作用呢?
1、防止重排序。**什么是重排序呢?比如Apple a = new Apple();,创建Apple对象的步骤如下:
(1)在堆中划出一片内存区域,用于存放Apple对象
(2)Apple对象初始化
(3)把堆中的地址赋值给栈中的a
但是,java多线程中,步骤(2)和(3)是有可能调换顺序的。假如先把地址赋值给了a,但是Apple对象还没有初始化。这个时候去判断null就已经不是空了,然后,就把这个对象返回去用,由于其还没有初始化,就有可能各种脏数据,出问题了。
2、线程可见性。**什么是可见性呢?某一个线程改变了公用对象或者变量,在短时间内,另外一个线程有可能是不可见的,因为每个线程都有自己的缓存区域。
综上所述,懒汉式4,即DCL + volatile的方式,是最比较值得推荐的实现方式。
5.静态内部类(推荐使用的方式)
public class PersonStatic {
private PersonStatic() {}
public static PersonStatic getInstance() {
return PersonStaticHolder.INSTANCE;
}
/**
* 静态内部类
*/
private static class PersonStaticHolder {
private static PersonStatic INSTANCE = new PersonStatic();
}
}
其实现原理和饿汉式差不多,解决了饿汉式加载类就加载的问题,因为外部类PersonStatic加载时,其内部类PersonStaticHolder是不会随着加载的。当第一次执行getInstance时,执行到PersonStaticHolder.Instance时,PersonStaticHolder类才开始被加载。其实不管多少个线程去调用getInstance方法,返回的都是第一次调用时创建的对象。这种方法不仅能保证了线程的安全性,也保证单例的唯一性,同时也延迟了单例的实例化。
缺点:假如PersonStatic创建时需要传入一个参数,比如Context,那么这种方式实现的单例是不支持。
6.枚举
1、定义一个需要单例的Person类
2、定义枚举类,用于保存Person类对象
public enum PersonEnum {
INSTANCE;
private Person mPerson;
PersonEnum() {
mPerson = new Person();
System.out.println("PersonEnum run");
}
public Person getPerson() {
return mPerson;
}
}
3、调用
public class Test {
public static void main(String[] args) {
Person p1 = PersonEnum.INSTANCE.getPerson();
Person p2 = PersonEnum.INSTANCE.getPerson();
System.out.println(p1 == p2); // true
}
}
首先需要明确两点,第一,枚举类的构造方法本身就只支持私有的,即外部是不能直接去new的。第二,JAVA里枚举类的创建本身就是在线程安全的情况下。基于以上两点,第一次调用PersonEnum.Instance实际上就会调用枚举类其自己构造方法,类似于我们创建对象一样,当执行构造方法后,就会创建一个Person对象存储在mPerson变量中。当第二次再调用PersonEnum.Instance时,Instance属性因为已经存在了,所以不会再执行枚举里的构造方法了,从而保证了单例的唯一性。
缺点:Android里并不推荐使用枚举类,有两个原因,第一,较多的枚举类会增加DEX文件大小。第二,枚举比普通常量占用内存大很多。