Java设计模式之单例模式
前言
单例模式是Java设计模式中最简单也是最常见的一种,它保证了在应用程序中,某个类只有一个实例存在。像配置文件的对象,数据库连接池,多线程的线程池,任务管理器等等,都是单例模式的典型应用。本文循序渐进,由简至繁,介绍单例模式的写法。
目录
单例模式写法
1.最原始的单例模式
public class Singleton {
// 私有静态的实例
private static Singleton singleton;
// 私有的构造方法
private Singleton() {
System.out.println("创建实例");// 打印输出,方便查看效果
}
// 公开的静态方法,获取单一实例
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 通过私有化构造方法,使应用程序无法从外部通过
new
来实例化此类的对象 -
私有化实例对象,防止外部通过
.
直接调用实例对象 - 通过公开的静态方法
getInstance()
,在方法内部检验类是否已经实例化,如果没有则实例化此类的对象,并返回对象
这种写法看似满足了单例模式的条件,实际上没有考虑到并发访问的情况。这里我们模拟一个并发环境下的测试。
// 测试并发环境下的结果
public class Main {
public static void main(String[] args) {
// 通过for循环创建100个线程,每个线程都调用Singleton.getInstance()
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Singleton singleton = Singleton.getInstance();
}).start();
}
}
}
运行结果:
会发现创建了不只一个实例,至于原因我们可以设想这样的情况:
- 并发访问开始,第一个调用
getInstance
方法的线程为线程A
-
线程A
判断完singleton
是null
后进入了if
块准备创造实例 - 但同时另外一个
线程B
在线程A
还未创造出实例之前,就又进行了singleton
是否为null
的判断 - 由于这时
singleton
依然为null
,所以线程B
也会进入if
块去创造实例 - 此时就有两个以上的线程都进入了
if
块去创造实例,结果就造成单例模式并非单例。
为了避免这种情况,我们就可以接着往下看懒汉模式。
2懒汉模式
public class Singleton {
// 私有静态的实例
private static Singleton singleton;
// 私有的构造方法
private Singleton() {
System.out.println("创建实例");// 打印输出,方便查看效果
}
// 公开的静态方法,获取单一实例,使用synchronized加锁
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种方法其实和上一种基本一样,唯一的区别在于getInstance
方法增加了synchronized
关键字来实现同步锁。重新进行并发测试,可以发现整个过程只会产生单一实例。
但是这种方法有个缺点,就是性能太低,因为每一次getInstance
都会加锁,而加锁过程是非常耗时的,而这里的锁只在实例被创建之前有意义,当实例被创建之后,这里的锁反而成了拖慢性能的累赘。
因此我们继续往下,学习双重校验模式。
3.双重校验
public class Singleton {
// 私有静态的实例
private static Singleton singleton;
// 私有的构造方法
private Singleton() {
System.out.println("创建实例");// 打印输出,方便查看效果
}
// 公开的静态方法,获取单一实例,使用双重检验锁
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这一种就属于教科书式的写法,只是在当前实例为null
,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了当实例已被创建后,由于锁机制产生无谓的线程等待时间。
这里进行了两次对于实例的非null
判断,但是作用不一样:
- 外层的
if
是为了减小无谓的性能开销,防止实例被创建后还继续触发机制 - 内层的
if
是为了防止并发条件下,多个线程等待进入同步代码块,造成创建多个实例的情况。
不过这样写依然有微小的缺陷,因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作,编译器和处理器可能会对指令进行重排序,从而造成异常(虽然这种情况很少见)。
因此我们可以引入JDK1.5之后的volatile
关键字,来修饰实例对象:
public class Singleton {
// 私有静态的实例
private volatile static Singleton singleton;
// 私有的构造方法
private Singleton() {
System.out.println("创建实例");// 打印输出,方便查看效果
}
// 公开的静态方法,获取单一实例,使用双重检验锁
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这么一来,可以避免指令重排序造成的异常情况。
4.饿汉模式
public class Singleton {
// 私有静态的实例
private static Singleton singleton = new Singleton();
// 私有的构造方法
private Singleton() {
System.out.println("创建实例");// 打印输出,方便查看效果
}
// 公开的静态方法,获取单一实例
public static Singleton getInstance() {
return singleton;
}
}
这种模式可以保证在任何情况,包括并发环境中永远获取单一实例,并且写法简单。但是缺点在于一旦我访问了Singleton的任何其他的静态域,就会造成实例的初始化,而事实是可能我们从始至终就没有使用这个实例,造成内存的浪费。
5.静态内部类
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return SingletonInstance.instance;
}
private static class SingletonInstance{
static Singleton instance = new Singleton();
}
}
定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonInstance
的类,只有在Singleton.getInstance()
中调用,由于私有的属性,他人无法使用SingletonInstance
,不调用Singleton.getInstance()
就不会创建实例。达到了懒加载的效果,即按需创建实例。
上一篇: java线程
下一篇: Java设计模式之单例模式