Java与设计模式之单例模式(上)六种实现方式
单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象和对话框对象等常被设计成单例。
应聘的时候,面试官经常会问,怎么保证某个类在程序运行过程中只有一个对象存在?此问题实际就是面试官在了解面试者对单例模式的了解。
总之,选择单例模式就是为了走出多头管理、政出多门的怪圈。本文介绍单例模式的六种实现方式,在《Java与设计模式之单例模式(下)》中分析如何使用Java实现线程安全的单例模式。单例模式的结构
单例模式的特点:
-
单例类只能有一个实例;
-
单例类必须自己创建自己的唯一实例;
-
单例类必须给所有其他对象提供这一实例。
单例模式有多种写法,各有利弊,现在我们来看看各种模式写法。
1. 饿汉式单例
结构图:
实现代码:
/**
* 饿汉式单例,线程安全
*/
public class EagerSingleton {
// 自行实例化,并用 static 和 final 修饰
private static final EagerSingleton instance = new EgerSingleton();
// 私有化构造方法
private EagerSingleton() {
}
// 对外发布,并用static修饰。静态公有工厂方法,返回唯一实例
public static EagerSingleton getInstance() {
return instance;
}
}
Singleton通过将构造方法限定为private避免了类在外部被实例化。在同一个虚拟机范围内,想调用其中的方法getInstance就必须使用static修饰,这样就可以通过类名.方法名访问EagerSingleton的唯一实例了;又因为静态方法里只能用静态成员,所以instance必须static化。
成员变量instance前可以不加final,因为静态方法只在编译期间执行一次初始化,也就是只会有一个对象。
饿汉式单例是线程安全的。当类被加载时,静态变量instance会被初始化;此时类的私有构造函数会被调用,从而单例类的唯一实例将被创建,以后不再改变,无需关注多线程问题,写法简单明了。
饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,然后每次调用的时候,就不需要再判断,节省了运行时间。如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都创建。
2. 懒汉式单例
/**
* 懒汉式单例模式(线程安全)
*/
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
之所以叫做懒汉式单例模式,主要是因为此种方法lazy loading,简单说就是什么时候调用,什么时候创建。它是典型的时间换空间。如果从源码中删除关键字synchronized,则是线程不安全的懒汉式单例模式。并发其实是一种特殊情况,同步锁锁的是对象,每次取对象的时候都加锁会浪费资源,因此,这种方式写出来的结构效率很低,不推荐。
3. 双重检查加锁单例
使用“双重检查加锁”的方式来实现单例可以既实现线程安全,又使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?
所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。
因为上面的懒汉式单例模式每次请求时都会添加同步锁,很浪费性能,所以在加锁之前先进行非空校验。
/**
* 双重检查加锁单例
*/
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton = null;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getSingleton() {
if (null == singleton) {
synchronized (DoubleCheckSingleton.class) {
if (null == singleton) {
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
先分析去掉volatile关键字的双重检查加锁。看似简单的一段赋值语句:
1 instance = new Singleton(); // 其实JVM内部已经转换为多条指令: 2 memory = allocate(); //1:分配对象的内存空间 3 ctorInstance(memory); //2:初始化对象 4 instance = memory; //3:设置instance指向刚分配的内存地址
但是经过重排序后如下:
1 memory = allocate(); //1:分配对象的内存空间 2 instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化 3 ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。
Volatile关键字的作用是禁止进行指令的重排序。相比于去掉volatile关键字的双重校验锁, 加上之后保证了线程安全,但是,性能降低了。这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
4. 静态内部类单例
这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了线程安全和类似懒汉式单例模式的延迟加载。
什么是类级内部类?
简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。
多线程缺省同步锁的知识
大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:
- 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
- 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
/**
* 静态内部类
*/
public class InnerStaticSingleton {
// 私有的静态内部类
private static class Holder {
private static InnerStaticSingleton instance = new InnerStaticSingleton();
}
private InnerStaticSingleton() {
System.out.println("Singleton has been loaded.");
}
public static InnerStaticSingleton getInstance() {
return Holder.instance;
}
}
当getInstance方法第一次被调用的时候,它第一次读取Holder.instance,导致Holder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建InnerStaticSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
5. 枚举单例
/**
* 枚举单例
*/
public enum EnumSingleton {
INSTANCE;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void otherMethod(){
System.out.println("Do something.");
}
}
这种方式是《Effective Java》作者Josh Bloch 提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,推荐!是更简洁、高效、安全的实现单例的方式。
测试用例:
public static void main(String[] args) {
EnumSingleton.INSTANCE.otherMethod();
System.out.println("-----------------------");
}
6. 登记式单例
登记式单例实际上维护的是一组单例类的实例,将这些实例存储到一个Map(登记簿)中,对于已经登记过的单例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。
import java.util.HashMap;
import java.util.Map;
/**
* 登记式单例类.<br/>
* 类似Spring里面的方法,将类名注册,下次从里面直接获取。
* @author east7
*/
public class RegisterSingleton {
//使用一个map来当注册表
private static Map<String, RegisterSingleton> map = new HashMap<String, RegisterSingleton>();
//静态块,在类被加载时自动执行,把 RegisterSingleton 自己也纳入容器管理
static {
RegisterSingleton single = new RegisterSingleton();
map.put(single.getClass().getName(), single);
}
//受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
protected RegisterSingleton() {
}
//静态工厂方法,返回此类惟一的实例
public static RegisterSingleton getInstance(String name) {
if (name == null) {
name = RegisterSingleton.class.getName();
System.out.println("name == null" + "--->name=" + name);
}
if (map.get(name) == null) {
try {
map.put(name, (RegisterSingleton) Class.forName(name).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一个示意性的商业方法
public String about() {
return "Hello, I am RegisterSingleton.";
}
public static void main(String[] args) {
RegisterSingleton single3 = RegisterSingle-ton.getInstance(null);
System.out.println(single3.about());
}
}
Reference
- https://www.cnblogs.com/alter888/p/9163612.html
- https://blog.51cto.com/13477015/2177185
- https://www.cnblogs.com/java-my-life/archive/2012/03/31/2425631.html
- https://www.cnblogs.com/twoheads/p/9723543.html
- JAVA与模式