单例简介
特点
- 内存中只有一个实例,节约内存,无需频繁创建,减少性能开销,提高系统运行效率
- 使用者无需关心类创建过程,整个项目中任何地方、任何时间开箱即用
缺点
- 单例模式没有抽象,扩展会有很大困难
- 单例类的职责过重,违背了“单一职责原则”
适用场景
- 适用于全局共享变量、方法,如统计在线人数、对接第三方Client等
- 常用配置和工具类如各种Config、Properties、JSONUtil、HTTPUtil等
单例常用方法
1. 饿汉模式
- 静态方法
public class Singleton {
private final static Singleton singleton = new Singleton();
private Singleton(){ }
public static Singleton getInstance() {
return singleton;
}
}
- 静态代码块
public class Singleton {
private final static Singleton singleton;
static {
singleton = new Singleton();
}
private Singleton(){ }
public static Singleton getInstance() {
return singleton;
}
}
小结
这两种方法使用比较简单,平时开发中用的也比较多。
优点
- 使用简单、快速上手、天然线程安全,由JVM保证每个类在内存中只有一个实例
缺点
- 不管是否用到,都会实例化,那么问题来了不使用为什么要创建
2. 懒汉模式
- 按需加载
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 线程安全
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
//线程安全
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 双重检查锁DCL(Double Checked Lock)
public class Singleton {
// 私有实例,volatile关键字,禁止指令重排。
private volatile static Singleton instance;
private Singleton() {}
// 公共获取实例方法(线程安全)
public static Singleton getInstance() {
//当instance不为null时大部分调用都没有锁,直接返回,性能好
if(instance == null ) {
// 一重检查不加锁提高并发性能
synchronized (Singleton.class) {
if(instance == null) {
// 二重检查确保创建线程安全
instance = new Singleton();
}
}
}
return instance;
}
}
小结
懒汉模式是针对饿汉模式不能按需加载进行的改进,第一种改进出现了线程不安全于是出现了第二种线程安全的方法,第二种加锁后线程安全又导致性能比较差,所以出现了第三种DCL双重检查锁方式。
优点
- 解决了饿汉模式不能按需加载的问题
缺点
- 出现了线程不安全、加锁性能差等问题,然后又打补丁,代码冗余
3. 基于内部类
内部类静态方法
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
private static class SingletonHolder {
private static Singleton singleton=new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
内部类静态代码块
public class Singleton { private static Singleton singleton; private Singleton() { } private static class SingletonHolder { private static Singleton singleton; static { singleton = new Singleton(); } } public static Singleton getInstance() { return SingletonHolder.singleton; } }
小结
基于内部类即解决了按需加载的问题,可以延迟加载,又解决了加锁导致的性能问题,是对饿汉模式和懒汉模式取其精华,去其糟粕,推陈出新,革故鼎新最好的体现。在我的一种适合容器化部署的雪花算法ID生成器一文中就使用该方法,延时加载获取到了REDIS_ID。
优化
- 使用时实例化对象,可以延迟加载
- 无锁,性能好
缺点
- 写法稍微复杂了点
4. 枚举
枚举单例
public enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("Hello World!");
}
}
public class Test {
public static void main(String[] args) {
Singleton.INSTANCE.sayHello();
}
}
枚举多例
public enum Singleton {
TOM("tom"),
JACK("jack");
private Singleton(String name){
this.name = name ;
};
private String name;
public void sayHello() {
System.out.println("My name is "+this.name);
}
}
public class Test {
public static void main(String[] args) {
Singleton.TOM.sayHello();
Singleton.JACK.sayHello();
}
}
小结
枚举方法一出,前面的方法都黯然失色了,看过刘慈欣《球状闪电》有一段话让我印象深刻,张彬花了一辈子时间用了很复杂的数据模型也没能解开球状闪电之谜,它怎么也没想到最终只是个电子而已。计算机系统如此复杂,最终执行的只是0和1,林将军说道:之所以失败,不是想得不够复杂,而是想得不够简单,感觉用枚举写单例就是对这句说最好的诠释。这种写法也是本文中唯一一种可以防止反射破坏单例的写法,Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。序列化的时候只将INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
优点
- 简洁、优雅
- 可防止反射破坏单例
缺点
- 有些人不知道这种写法
5. hutoll单例池
public class Dog {
private Dog(){ }
public void say() { System.out.println("汪汪"); }
}
public static void main(String[] args) {
Dog dog = Singleton.get(Dog.class);
}
}
Singleton官方文档
小结
hutool 的Singleton类就是通过一个ConcurrentHashMap将实列化的对象存入其中,如果Map中不存在对象就通过反射创建一个对象,确保Map中只有一个对象。
public final class Singleton {
private static final ConcurrentHashMap<String, Object> POOL = new ConcurrentHashMap<>();
private Singleton() {
}
/**
* 获得指定类的单例对象<br>
* 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象<br>
* 注意:单例针对的是类和参数,也就是说只有类、参数一致才会返回同一个对象
*
* @param <T> 单例对象类型
* @param clazz 类
* @param params 构造方法参数
* @return 单例对象
*/
public static <T> T get(Class<T> clazz, Object... params) {
Assert.notNull(clazz, "Class must be not null !");
final String key = buildKey(clazz.getName(), params);
return get(key, () -> ReflectUtil.newInstance(clazz, params));
}
public static <T> T get(String key, Func0<T> supplier) {
Object value = POOL.get(key);
if(null == value){
POOL.putIfAbsent(key, supplier.callWithRuntimeException());
value = POOL.get(key);
}
return (T) value;
}
}
值得注意的是虽然我们将Dog构造函数设置成了private,无法new Dog()但是还是可以通过反射来创建一个实例了,反射是会破环单例的,代码如下:
public static void main(String[] args) {
Dog dog= ReflectUtil.newInstance(Dog.class);
System.out.println(dog == Singleton.get(Dog.class)); //false
}
优点
- 不需要自己写任务代码,使用别人的轮子就简单多了
缺点
- 需要引入第三方包
6. Spring @Component、@Service...
相信如今大部分项目都会用到Spring框架,都用到过了@Component、@Service、@Repository、@Controller,当加到这些注解后默认都是单例了。
@Component
public class Singleton {
private Integer id = 0;
private Singleton() {
}
public Integer sayHello() {
System.out.println(String.format("id is %d", id++));
return id;
}
}
@RestController
public class TestController {
@Resource
private Singleton singleton;
@GetMapping("/test")
public ResponseEntity<Integer> test() {
return ResponseEntity.ok(singleton.sayHello());
}
}
上面的Demo中当我们每次请求 /test接口时,Singleton中的成员变量id都会+1,这也说明每次请求singleton是同一个对象了。但是当我们在Singleton和TestController上增加@Scope("prototype")情况就变了。
@Component
@Scope("prototype")
public class Singleton {
}
@RestController
@Scope("prototype")
public class TestController {
}
我们可以看出,每次请求id都是1,说明不是单例了,需要指出的是只在Singleton添加@Scope("prototype")是不够的,因为RstController本身也是单例,所以要在二者上都加上@Scope("prototype")才能让Singleton变成单例。
小结
使用Spring的单例非常简单,增加一个注解就可以了,不过需要注意的是加上@Scope("prototype")会让它变成多例了,Bean也不会被Spring托管了,只是每次帮我们通过代码new了一个对象,具体原理可以参考 Spring的@Scope注解 prototype
优点
- 此时无声,胜有声,不知不觉中就使用了单例
缺点
- 隐藏了很多细节,不知道细节可能引起大问题
总结
单例的写法有很多,可能还有我不知道的写法,每种都有其优缺点,适用场景也不同,所以需要根据实际情况选择具体的方法。多知道一种方法,可能在下次遇到特定场景时会给你多一种选择,这种选择可能会比饿汉模式、懒汉模式更简洁、更优雅。