概念
单例属于创建型模式,在
设计模式:可复用面向对象软件的基础中给出的定义:保证一个类仅有一个实例,并提供一个全局访问点。
优点
在内存中只有一个实例,减少内存开销,避免频繁创建和销毁实例
避免对资源的多重占用
实现方式
对象的产生是由构造器完成的,单例就是通过将构造函数变为私有的,使外部不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对像以及一个访问该对象的静态方法
饿汉式
实现
/**
* 饿汉式单例.
* 通过static的静态初始化方式,在该类第一次被加载的时候,
* 就有一个SimpleSingleton的实例被创建出来了.
* 这样就保证在第一次想要使用该对象时,他已经被初始化好了.
*/
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
测试
@Test
public void testSingleton() {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
assertThat(s1 == s2);
}
JDK中单例的实现
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
以上代码为JDK中Runtime类的部分实现,在该类第一次被classloader加载的时候,这个实例就被创建出来了。
一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。
静态内部类方式
实现
/**
* 静态内部类式.
*/
public class Singleton2 {
private static class SingletonHolder {
private static final Singleton2 instance = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return SingletonHolder.instance;
}
}
说明
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程;
饿汉式:只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果)
静态内部类式: Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance
懒汉式
/**
* 懒汉模式.
* 这种模式存在线程安全问题.
*/
public class SingletonLazy {
private static SingletonLazy singletonLazy;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (null == singletonLazy) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}
测试
@Test
public void testSingletonLazyByCdl() throws Exception {
countDownLatch = new CountDownLatch(10);
Set<SingletonLazy> set = new HashSet<>();
for (int index = 0; index < 10; index++) {
Runnable run = () -> {
try {
SingletonLazy lazy = SingletonLazy.getInstance();
set.add(lazy);
} finally {
countDownLatch.countDown();
}
};
scheduler.submit(run);
}
countDownLatch.await();
scheduler.shutdown();
set.forEach(s -> {
System.out.println("Singleton is=" + s.toString());
});
assertThat(set).hasSize(1);
}
反复执行测试类,会发现报错提示
这种懒汉式单例确实存在线程安全问题。在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。
双重校验锁式
/**
* 使用双重校验锁方式实现单例.
*/
public class SingletonLazy2 implements Serializable {
//volatile关键字:防止指令重排序
private static volatile SingletonLazy2 singletonLazy2;
private SingletonLazy2() {
}
private Object readResolve() {
return singletonLazy2;
}
//对外提供获取实例的静态方法,对代码块加锁,缩小锁的范围
public static SingletonLazy2 getInstance() {
if (null == singletonLazy2) {
synchronized (SingletonLazy2.class) {
if (null == singletonLazy2) {
singletonLazy2 = new SingletonLazy2();
}
}
}
return singletonLazy2;
}
}
说明
使用 synchronized 同步代码块的方式缩小锁的范围
使用 volatile 关键字禁止指令重排序: new SingletonLazy2()在JVM类加载机制中,在准备阶段(preparation)存在给 singletonLazy2 赋值事务可能;
定义 readResolve 方法防止序列化对单例的破坏
总结
-
当一个类的对象只需要或者只可能有一个时,应该考虑单例模式。
-
如果一个类的实例应该在JVM初始化时被创建出来,应该考虑使用饿汉式单例。
-
如果一个类的实例不需要预先被创建,也许这个类的实例并不一定能用得上,也许这个类的实例创建过程比较耗费时间,也许就是真的没必须提前创建。那么应该考虑线程安全的懒汉式单例。
代码
Github: 单例Code
最后
要记得什么是设计模式,也要忘记什么是设计模式