一 单例模式简介
1.1 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.2 为什么要用单例模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、注册表、日志对象等对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
简单来说使用单例模式可以带来下面几个好处:
- 对于频繁使用的对象,可以省略创建对象花费的时间,对于那些重量级的对象而言,是非常可观的一笔系统开销。
- 由于new操作的次数减小,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
1.3 为什么不使用全局变量确保一个类只有一个实例呢?
全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
但是,如果一个静态变量长时间未被使用,这样会造成资源的浪费。利用单例模式德华,我们就可以实现在使用的时候才创建对象,避免不必要的资源浪费。
二 单例模式的实现
通常单例模式在Java语言中,由两种构建方式:
- 饿汉方式。指全局的单例实例在类加载时构建。
- 懒汉方式。指全局的单例实例在第一次被使用时构建。
不管是哪种创建方式,它们通常都存在下面几点相似处:
- 单例必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被初始化。
- instance成员变量和uniqueInstance方法必须是static的。
2.1 饿汉方式(线程安全)
public class Singleton {
//在静态初始化器中创建单例实例,这段代码保证了线程安全
private static Singleton uniqueInstance = new Singleton();
//Singleton类只有一个构造方法并且是被private修饰的
private Singleton() {
}
public static Singleton getInstance() {
return uniqueInstance;
}
}
所谓“饿汉方式”就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,变浪费空间,典型的空间换时间,每次调用的时候,就不需要在判断,节省了运行时间。
懒汉式(非线程安全和synchronized关键字线程安全版本)
public class Singleton1 {
private static Singleton1 uniqueInstance;
private Singleton1() {
}
//没有加入synchronized关键字的版本是线程不安全的
public static Singleton1 getInstance() {
//判断当前单例是否已经存在,若存在则返回,不存在则再建立单例
if (uniqueInstance == null) {
uniqueInstance = new Singleton1();
}
return uniqueInstance;
}
}
所谓“懒汉式”就是说单例实例在第一次被使用时创建,而不是在JVM在加载这个类时就马上创建此唯一的单例实例。
但是上面这种方式很明显是线程不安全的,如果多个线程同事访问getInstance()方法时就会出现问题。如果想要保证线程安全,一种比较常见的方式就是在getInstance()方法前面加上synchronized关键字,如下:
public static synchronized Singleton1 getInstance() {
//判断当前单例是否已经存在,若存在则返回,不存在则再建立单例
if (uniqueInstance == null) {
uniqueInstance = new Singleton1();
}
return uniqueInstance;
}
我们知道synchronized关键字偏重量级锁。虽然早JavaSE1.6之后synchronized关键字进行了各种优化,主要包括:为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,之后执行效率有了明显提升。
但是在程序中每次使用getInstance()都要经过synchronized加锁这一层,这难免会增加getinstance()的方法的时间消耗,而且还可能会发生阻塞。下面介绍到的双重检查加锁版本
就是为了解决这个问题而存在的。
2.3 懒汉式(双重检查加锁版本)
利用双重检查加锁(double-checked locking),首先检查是否实例已经被创建没如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的结果。
public class Singleton2 {
//volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理niqueInstance变量
private volatile static Singleton2 uniqueInstance;
private Singleton2() {
}
public static Singleton2 getInstance() {
//检查实例,如果不存在,就进入同步代码块
if (uniqueInstance == null) {
synchronized (Singleton2.class) {
//进入同步代码块后,再检查一次,如果仍是null,才创建实例
if (uniqueInstance == null) {
uniqueInstance = new Singleton2();
}
}
}
return uniqueInstance;
}
}
很明显,这种方式相比于使用synchronized关键字的方法,可以大大减少getinstance()的时间消耗。
关于volatile关键字可以参考《Java多线程学习(三)volatile关键字》
2.4 懒汉式(登记式/静态内部类方式)
静态内部实现的单例式懒加载的且线程安全。
只用通过显示调用getinstance方法时,才会显示装载SingletonHolder类,从而实例化instance(只有第一次使用这个单例的实例的时候才加载,同事不会有线程安全问题)。
public class Singleton3 {
private static class SingletonHolder {
private static final Singleton3 INSTANCE = new Singleton3();
}
private Singleton3() {
}
public static final Singleton3 getInstance() {
return SingletonHolder.INSTANCE;
}
}
饿汉式(枚举方式)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。(如果单例实现了Serializable接口。默认情况下每次反序列化总会创建一个新的实例对象,关于单例与序列化的问题可以查看这一篇文章《单例与序列化的那些事儿》),同时这种方式也是《Effective Java 》以及《Java与模式》的作者推荐的方式。
public enum Singleton4 {
//定义一个枚举的元素,它就是Singleton4的一个实例
INSTANCE;
public void doSomeThing(){
System.out.println("枚举方法实现单例");
}
}
使用方法:
public class Singleton4Test {
public static void main(String[] args) {
Singleton4 singleton4 = Singleton4.INSTANCE;
singleton4.doSomeThing();
}
}
2.6 总结
主要介绍了以下几种方式实现单例模式:
- 饿汉方式(线程安全)
- 懒汉式(非线程安全和synchronized关键字线程安全版本)
- 懒汉式(双重检查加锁版本)
- 懒汉式(登记式/静态内部类方式)
- 饿汉式(枚举方式)