网站首页 > 文章精选 正文
单例模式(Singleton Pattern)是Java中最简单的设计模式之一,属于创建型模式。它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,排除线程不安全的风险。这个类提供了一种访问其唯一的对象的方式。
相关阅读:
Spring的设计模式快速入门干货
快速理解设计模式之创建型模式
懒汉式,线程安全
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。 getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
饿汉式
这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
笔者最喜欢用的方式,源码里使用这种形式也很多
静态内部类
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
双重校验锁
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重检测的FAQ
1. 为何要同步?
多线程情况下,若是A线程调用getInstance,发现instance为null,那么它会开始创建实例,如果此时CPU发生时间片切换,线程B开始执行,调用getInstance,发现instance也null(因为A并没有创建对象),然后B创建对象,然后切换到A,A因为已经检测过了,不会再检测了,A也会去创建对象,两个对象,单例失败。因此要同步。
2. 同步为何不用public synchronized static SingletonClass getInstance(),也就是说为何不同步这个方法,而要同步下面的语句?
因为synchronized修饰的同步块的运行要比一般的代码段慢,如果经常调用getInstance,那么性能问题就得考虑了。
有关synchronized的更详细讲解,请看详解Java多线程锁之synchronized
3. 最外层为何要有if (instance == null)判断?
减少同步代码块的运行次数,从而减少不必要的加锁操作。
4. instance为何要有volatile修饰?
结论:volatile能够防止指令的重排序。
有关volatile的更多知识,请看Java多线程的可见性与有序性
JVM实现可以自由的进行编译器优化。而我们创建变量的步骤:
- 申请一块内存,调用构造方法进行初始化。
- 分配一个指针指向这块内存。
而这两个操作,JVM并没有规定谁在前谁在后,那么就存在这种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了。
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。
猜你喜欢
- 2024-12-26 Java高级:条件队列与同步器Synchronizer的原理+AQS的应用
- 2024-12-26 浅谈Java多线程与并发原理 java多线程并发调用接口
- 2024-12-26 Java 基础(四)集合源码解析 List java集合linkedlist
- 2024-12-26 synchronized和lock的区别 54.synchronized 和 lock 有什么区别?
- 2024-12-26 异步 vs 同步:程序员必备的核心知识,理解这两者差异,你就是高手
- 2024-12-26 ArrayList 、 LinkedList、Vector的区别
- 2024-12-26 java面试基础题(实战后的总结) java面试必考300题
- 2024-12-26 synchronized底层细究(硬核) synchronized底层原理是什么
- 2024-12-26 为什么 95% 的 Java 程序员,都是用不好 Synchronized?
- 2024-12-26 100+道高频Java面试题 java面试高频知识点
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)