单例模式是开发中经常用到的设计模式,单例模式的写法有很多种,主要有5种方式:饿汉式,懒汉式,双重校验锁,静态内部类和枚举。其中,一些方式有不同的写法。单例模式是确保一个类只有一个实例,必须自己创建自己的唯一实例。在计算机系统中,线程池、缓存、日志对象、打印机等程序对象常被设计成单例。
创建单例模式的核心知识:
(1)单例类的构造方法必须私有化(采用private修饰);
(2)在类内部产生该实例化对象,并将其封装成private static类型;
(3)定义一个静态方法返回该类的唯一实例。
1. 饿汉式
/**
* 单例模式实现方式1:饿汉式:类加载马上创建实例。
* 变量是静态的,是属于类的,类一加载就创建。内存位于方法区。所有实例共享一个静态变量。
* 特点:线程安全,会浪费内存。基于classloder机制避免了多线程的同步问题
* 类一加载就创建静态变量实例,会一直存在内存中,直到类被卸载。
*/
public class Singleton {
private Singleton() {}
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
所谓饿汉式就是在Singleton一加载时,就创建了静态变量实例instance,这种方式线程安全,不存在线程同步问题。但缺点是:创建的实例会一直存在于内存中,直到类被卸载。比较浪费空间。
对于饿汉式实现单例模式,还有一种写法是利用静态代码块来实现,其原理与上边的相同:
/**
* 单例模式实现方式1:饿汉式。利用静态代码块实现
* 特点:与第一种方法一样,线程安全。
*/
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
类一加载后,静态代码块就执行,所以就创建了实例。
2. 懒汉式
所谓懒汉式单例模式,就是在需要的时候才创建实例,懒加载模式。
/**
* 单例模式实现方式2:懒汉式:延迟加载模式,在需要的时候才创建
* 特点:节约内存,但是线程不安全。类实例不声明成final。
* 多个线程同时访问,并发环境下,可能会出现多个实例。
*/
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
从上边的代码可以看出,这种懒汉式方法,线程不安全,当多个线程同时访问时,可能会创建多个实例。
3. 懒汉式简单同步方法实现
由于懒汉式实现的单例模式存在线程安全问题,解决同步问题利用synchronized关键字,有两种方式:一种是在方法上加synchronized关键字同步方法;第二种是同步代码块。下边是利用第一种方式实现线程同步:
/**
* 单例模式实现方式3:懒汉式,简单实现线程安全
* 同步的两个方式:1:同步方法 2:同步代码块
* 特点: synchronized关键字避免了出现多个类实例
* 同步方法反复调用,效率低。
*/
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上边的方法可以实现线程同步,但是同步方法可能会被反复调用,效率低。
4 懒汉式同步块双重校验锁实现
/**
* 单例模式实现方式4:懒汉式,双层检验,效率较高
* 特点:线程安全方法利用了同步代码块。
* 避免了同步方法被反复调用,效率高。
* instance声明时,用volatile关键字修饰,保证其可见性。
*/
public class Singleton {
private Singleton() {}
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种加双重锁的方法避免了同步方法被反复调用,效率较高。
volatile关键字:
volatile是要保证可见性,即instance实例化后马上对其他线程可见,而synchronized能同时保证原子性和可见性,同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,保证当其他线程再进入的时候,在主存中读取到的就是最新的变量内容了。所以只要synchronized就够了。
假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行instance = new Instance(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行instance的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后instance便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整(没有完成初始化)的Instance对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。
5 懒汉式静态内部类实现
/**
* 单例模式实现方式5:懒汉式,利用静态内部类实现,效率较高
* 特点:线程安全。避免了同步带来的性能影响,效率最高。
* 静态内部类被调用的时候才自动创建。
*/
public class Singleton {
private static class SingleLazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
return SingleLazyHolder.INSTANCE;
}
}
这种方法与第一种的区别就是第一种类一加载intance实例就被创建了,不管这个实例是否需要。
而这种方式是
Singleton
类被装载了,
instance
不一定被初始化。因为
SingletonHolder
类没有被主动使用,只有显示通过调用
getInstance
方法时,才会显示装载
SingletonHolder
类,从而实例化
instance
。如果实例化
instance
很消耗资源,我想让他延迟加载,另外一方面,我不希望在
Singleton
类加载时就实例化,因为我不能确保
Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化
instance
显然是不合适的。
6 枚举方式实现
/**
* 单例模式实现方式6:枚举式。
* 特点:它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
* 在JDK1.5后才加入的,用的比较少。
* 调用方式:Singleton.INSTANCE
*/
public enum Singleton {
INSTANCE;
}
总结:
事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效,只有枚举方式可行。此问题在此处不做讨论,地认为反射机制不存在。
实现单例模式的方式有很多种,在平时开发中,一般采用第一种饿汉式或者第五种内部类实现的懒汉式的方式。