如果一个类只允许创建一个对象(或实例),那么这个类就是一个单例类,这种设计模式就称为单例设计模式 ( Singleton Design Patter ),简称单例模式 ( Singleton Pattern ),从业务概念方面来讲,如果菜个类包含的数据在系统中只应保存一份,那么这个类就应该被设计为单例类。例如唯一递增工D 生成器类,如果程序中有两个ID生成器对象,那么有可能生成重复。
单例模式要注意的几个点
“饿汉”式的实现比我简单。在加载类时,实例就己经被创建并初始化,因此实例的创建过程是线程安全的。不过,这种实现方式不支持延迟加载,实例是提前创建好的,而非在使用时才创建。因此,这种实现方式被称为 “饿汉” 式。具体的代码实现如下所示:
HungryIdGenerator.java
package com.alian.singleton;
import java.util.concurrent.atomic.AtomicLong;
public class HungryIdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final HungryIdGenerator instance = new HungryIdGenerator();
private HungryIdGenerator() {}
public static HungryIdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
// HungryIdGenerator 类使用举例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Long id = HungryIdGenerator.getInstance().getId();
System.out.println(id);
}
}
}
与 “饿汉” 式相反的就有 “懒汉” 式,相比 “饿汉” 式 “懒汉” 式支持延迟加载,实例的创建和初始化推迟到真正使用时才进行。具体代码如下:
LazyIdGenerator.java
package com.alian.singleton;
import java.util.concurrent.atomic.AtomicLong;
public class LazyIdGenerator {
private AtomicLong id = new AtomicLong(0);
private static LazyIdGenerator instance;
private LazyIdGenerator() {
}
public static synchronized LazyIdGenerator getInstance() {
if (instance == null) {
instance = new LazyIdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
// LazyIdGenerator 类使用举例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Long id = LazyIdGenerator.getInstance().getId();
System.out.println(id);
}
}
}
“饿汉式” ,提前创建和初始化实例,如果占用的系统资源很多,就导致启动时间变长,甚至若系统资源不够,系统可能就OOM了,不过也是一个好事,也能提前发现问题;但是只要系统启动,那么后续对请求的处理都会很快。
“懒汉式” ,提供服务的时候创建和初始化实例,虽然加快系统启动,但是一旦服务器请求并发高,上面使用了 synchronized 可能会出现性能瓶颈。
“饿汉式” 不支持延迟加载; “懒汉式” 不支持高并发。我介绍一种既支持懒加载,又支持高并发的单例模式的实现方式:双重检测。
DCLIdGenerator.java
package com.alian.singleton;
import java.util.concurrent.atomic.AtomicLong;
public class DCLIdGenerator {
private AtomicLong id = new AtomicLong(0);
private static volatile DCLIdGenerator instance;
private DCLIdGenerator() {
}
public static DCLIdGenerator getInstance() {
if (instance == null) {
synchronized (DCLIdGenerator.class){
if (instance == null) {
instance = new DCLIdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
// DCLIdGenerator 类使用举例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Long id = DCLIdGenerator.getInstance().getId();
System.out.println(id);
}
}
}
CPU指令重排导致对象被关键字new创建并赋值给instance之后,还没有来得初始化(比如构造方法的代码逻辑),就被另外一个线程使用到未完全初始化的对象。所以我们给instance加了关键字:volatile。
使用静态内部类实现的单例模式比双重检测更加的简单,静态内部类类似 “饿汉式” ,但是又能做到延迟加载。具体代码实现如下:
InnerClassIdGenerator.java
package com.alian.singleton;
import java.util.concurrent.atomic.AtomicLong;
public class InnerClassIdGenerator {
private AtomicLong id = new AtomicLong(0);
private static InnerClassIdGenerator instance;
private InnerClassIdGenerator() {
}
private static class SingletonHolder {
private static final InnerClassIdGenerator instance = new InnerClassIdGenerator();
}
public static InnerClassIdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
// InnerClassIdGenerator 类使用举例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Long id = InnerClassIdGenerator.getInstance().getId();
System.out.println(id);
}
}
}
SingletonHolder是一个静态内部类,当外部类InnerClassIdGenerator加载时不会加载SingletonHolder,只有当方法getInstance()被调用时,SingletonHolder才会加载,才会创建实例instanceinstance。而instance的唯一性和创建过程的线程安全都是由JVM保证的。所以说这种方式既实现了懒加载,又保证了线程的安全。
基于枚举的单例模式的实现方式,通过Java枚举类型本身的特性,保证了实例创建的线程安全和实例的唯一性。具体代码实现如下所示:
EnumIdGenerator.java
package com.alian.singleton;
import java.util.concurrent.atomic.AtomicLong;
public enum EnumIdGenerator {
instance;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
// EnumIdGenerator 类使用举例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Long id = EnumIdGenerator.instance.getId();
System.out.println(id);
}
}
}
当涉及到单例模式的实现时,不同的方法有其独特的优点和缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
在选择单例模式的实现方式时,需要根据具体需求权衡其优缺点。如果需要保证线程安全,最好选择双重检查锁定、静态内部类或枚举实现。如果对性能要求不高或者可以提前创建实例,饿汉式是一个简单且可靠的选择。如果需要延迟加载,并能确保线程安全,懒汉式也是一个不错的选择。