创建单例
关键点:
- 构造函数必须是private访问权限,避免外部直接new一个对象;
- 需要考虑创建时的线程安全问题;
- 获取单例对象时的性能是否高(是否加锁);
- 是否支持延迟加载;
实现方式:
饿汉式:
饿汉式是在类加载时就创建了对象,所以创建对象的过程是线程安全的。这种实现方式有这些特点:
-
不支持延迟加载,初始状态就加载了对象;
-
没有线程安全问题;
-
不需要加锁;
饿汉式的示例代码如下:
public class Logger {
private static final Logger instance = new Logger();
private Logger() {};
public static Logger getInstance() {
return intance;
}
}
懒汉式:
懒汉式是给单例类加锁,支持延迟加载,但是在频繁使用、并发高的场景下会出现性能瓶颈,它有这些特点:
- 支持延迟加载;
- 没有线程安全问题;
- 将锁加在了对象上,导致每次只能串行获取对象,性能不高;
懒汉模式的样例代码如下:
public class Logger {
private static Logger instance;
private Logger() {};
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return intance;
}
}
双重检测:
双重检查是一种既支持延迟加载、又支持高并发的单例实现方式,它的特点有:
- 支持延迟加载,没有线程安全问题;
- 需要加锁,将锁加在getInstance()方法内部;
示例代码如下:
public class Logger {
private static Logger instance;
private Logger() {};
public static Logger getInstance() {
if (instance == null) {
synchronized(Logger.class) { // 双重检测
if (instance == null) {
instance = new Logger();
}
}
}
return intance;
}
}
静态内部类:
通过静态内部类来创建单例是一种相对安全的实现方式。在外部类被加载时、并不会立即加载静态内部类,当通过getInstance()方法初次加载静态内部类时,JVM保证只有一个线程对这个类进行初始化,因此这种方式既能实现懒加载、又能保证线程安全。它有这些特点:
- 支持延迟加载;
- 没有线程安全问题;
- 不需要加锁;
示例代码如下:
public class Logger {
private Logger() {};
// 静态内部类
private static class SingletonHolder {
private static final Logger instance = new Logger();
}
public static Logger getInstance() {
return SingletonHolder.instance;
}
}
枚举类:
通过Java枚举类本身的特性、即JVM保证枚举对象的唯一性,来保证实例创建的线程安全性和实例的唯一性。它具有以下特性:
-
不支持延迟加载;
-
没有线程安全问题;
-
不需要加锁;
枚举类实现方式的示例如下:
public enum Logger {
INSTANCE;
}
使用单例
单例使用场景
大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。
单例模式存在的问题
- 单例对OOP特性的支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数(可以实现,但是很麻烦)
单例模式的使用范围
单例即唯一,但是这个唯一是相对的,我们通常说的单例,是指在Java进程中唯一,但是在部分场景下,需要实现类唯一、线程唯一、集群唯一等相对的唯一。
进程唯一
进程唯一是我们通常所说的单例。我们编写的代码,通过编译、链接、组织在一起,构成了一个操作系统可执行的文件;在进程启动后,会开辟独立的内存空间,用于保存程序+数据。线程间是共享进程里的内存地址空间,因此单例类在进程中只能生成一个对象、单例对象在线程间也是唯一的。
线程唯一
线程唯一是指单例对象在线程内是唯一的,但是在进程内(线程间)不唯一。在实现的时候需要用hash表保存已经初始化了的单例对象;Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例
public class IdGenerator{
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, Logger> instances = new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}
public long getId() {
return id.incrementAndGet();
}
}
集群唯一
我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
多例模式
多例”指的是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象、或者每个指定的类里只能创建一个单例对象(Logger对象)。