java设计模式之单例模式(附多种实现方式)
一、单例模式介绍
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。简单说就是,整个程序有且仅有一个由该类提供的实例对象。当频繁创建对象需要消耗大量资源,或者在java应用中需要使用唯一全局实例变量时,都可以使用单例模式。
特点:
- 单例类只能有一个实例;
- 单例类持有自己类型的属性;
- 单例类对外提供获取该实例的静态方法。
主要优点:
- 避免频繁的创建销毁对象,可以提高性能;
- 在内存中只有一个对象,节省内存空间。
二、单例模式实现方式
- 饿汉式
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
private static final Singleton singleton = new Singleton();
// 提供对外的静态访问方法
public static Singleton getSingletonInstance() {
return singleton;
}
}
测试方法:
public class SingletonTest {
public static void main(String[] args) {
int count = 5;
CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
new Thread(() -> {
try {
latch.await();
System.out.println(Thread.currentThread().getName() + "-->" + Singleton.getSingletonInstance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
}
}
测试结果:
Thread-3-->learnbymaven.singleton.ehan.Singleton@464aa0d1
Thread-4-->learnbymaven.singleton.ehan.Singleton@464aa0d1
Thread-2-->learnbymaven.singleton.ehan.Singleton@464aa0d1
Thread-1-->learnbymaven.singleton.ehan.Singleton@464aa0d1
Thread-0-->learnbymaven.singleton.ehan.Singleton@464aa0d1
可以明显看到,程序中获取的实例是唯一的。
优点:实现方式简单、线程安全。
缺点:可能从始至终未使用过这个实例,造成内存的浪费。
- 懒汉式,线程不安全
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
private static Singleton singleton;
// 提供对外的静态访问方法
public static Singleton getSingletonInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
测试结果:
Thread-1-->learnbymaven.singleton.Singleton@35c2bd27
Thread-0-->learnbymaven.singleton.Singleton@45e76c2a
Thread-2-->learnbymaven.singleton.Singleton@45e76c2a
Thread-4-->learnbymaven.singleton.Singleton@45e76c2a
Thread-3-->learnbymaven.singleton.Singleton@35c2bd27
可以看到该实例并不是唯一的。
优点:实现方式简单、延迟加载。
缺点:线程不安全,只能适用于单线程应用。
- 懒汉式,线程安全
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
private static Singleton singleton;
// 提供对外的静态访问方法
public static synchronized Singleton getSingletonInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
测试结果:
Thread-3-->learnbymaven.singleton.Singleton@77ddb68
Thread-4-->learnbymaven.singleton.Singleton@77ddb68
Thread-0-->learnbymaven.singleton.Singleton@77ddb68
Thread-1-->learnbymaven.singleton.Singleton@77ddb68
Thread-2-->learnbymaven.singleton.Singleton@77ddb68
此方法只是在懒汉式线程不安全的基础上加上了synchronized关键字,可以看到获取的实例是唯一的。
优点:实现方式简单、线程安全、延迟加载。
缺点:使用synchronized关键字加锁影响效率,并发性能低。
- 双检锁/双重校验锁
错误示范(不加volatile关键字):
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
// 没有加volatile关键字
private static Singleton singleton;
// 提供对外的静态访问方法
public static Singleton getSingletonInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这段代码看起来没有问题,可能你放在多线程下测试也是可以通过的,但是其中却有一个致命隐患——指令重排序。
java中jvm初始化一个对象实际上可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
由于这种情况,在多线程环境中,有的线程可能访问到初始化未完成的对象。
优点:延迟加载、效率高。
缺点:可能由于jvm编译器的指令重排序导致bug。
正确示范(使用volatile关键字):
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
// 没有加volatile关键字
private static volatile Singleton singleton;
// 提供对外的静态访问方法
public static Singleton getSingletonInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
优点:线程安全、延迟加载、性能高。
缺点:实现方式较复杂,可能忘记使用volatile关键字而留下隐患。
- 静态内部类
public class Singleton {
// 私有化构造函数,避免其他地方创建该实例
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
// 提供对外的静态访问方法
public static Singleton getSingletonInstance() {
return SingletonHolder.singleton;
}
}
这种方式也是可以延迟加载的,由jvm类加载机制可知,只有通过显式调用 getSingletonInstance方法时,才会显式装载 SingletonHolder 类,从而实例化 singleton。
优点:实现方式简单、线程安全、延迟加载。
- 枚举
public enum Singleton {
MySingleton
}
优点:实现方式简单、线程安全、自动支持序列化机制。
不过实际工作中使用枚举类的方式实现单例模式比较不常见。