单例模式可以说是整个设计中最简单的模式之一,而且这种方式即使在没有看设计模式相关资料也会常用在编码开发中。
因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。
综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
单例模式所出现的场景非常简单也是日常开发所能见到的,例如:
- 数据库的连接池不会反复创建Spring中一个单例模式bean的生成和使用在我们平常的代码中需要设置全局的一些属性保存
单例模式有两种类型:
懒汉式:默认不会实例化,在真正需要使用对象时才去创建该单例类对象饿汉式:在类加载时已经创建好该单例类对象,等待被程序使用 1、懒汉模式(线程不安全)
public class SingletonLanHan {
private static volatile SingletonLanHan INSTANCE = null;
private SingletonLanHan() {
}
public static SingletonLanHan getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLanHan();
}
return INSTANCE;
}
}
单例模式有一个特点就是不允许外部直接创建,也就是new SingletonLanHan(),因此这里在默认的构造函数上添加了私有属性private。目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成一堆人在抢厕所,就会造成多个同样的实例并存,从而没有达到单例的要求。往下看↓ 2、懒汉模式(线程安全)
我们来回顾一下懒汉式的核心方法
public static SingletonLanHan getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLanHan();
}
return INSTANCE;
}
这个方法其实是存在问题的,试想一下,如果两个线程同时判断INSTANCE为空,那么它们都会去实例化一个SingletonLanHan对象,这就不是单例了。所以,我们需要解决线程安全问题。
那么最简单的方法就是在方法上加同步锁,如以下操作
public class SingletonLanHan {
private static volatile SingletonLanHan INSTANCE = null;
private SingletonLanHan() {
}
public static synchronized SingletonLanHan getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLanHan();
}
return INSTANCE;
}
}
该方法虽然线程安全,但由于把锁放到方法上后,所以的访问都因需要锁占用资源的浪费。如果不是特殊情况,不推荐使用该方法。
3、双重锁校验(线程安全)那么优化性能的目标:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
public static SingletonLanHan getSingLetonLanHan() {
if (SINGLETonLANHAN == null) {
synchronized (SingletonLanHan.class) {
if (SINGLETonLANHAN == null) {
SINGLETonLANHAN = new SingletonLanHan();
}
}
}
return SINGLETONLANHAN;
}
上面第2行代码,如果SINGLETONLANHAN不为空,则直接返回对象,不需要获取锁;而如果多个线程发现SINGLETONLANHAN为null,则进入分支;第3行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,那么该线程会继续判断是否SINGLETONLANHAN是否为空,因为SINGLETONLANHAN有可能已经被之前的线程实例化了。那么后面进来的线程在执行到第四行代码时,发现SINGLETONLANHAN不为空,则不会创建新的对象,直接返回对象即可。而之后所有进入该方法的线程都不会去获取锁,在第一行就会被校验住,然后直接返回对象。
指令重排!!!
指令重排序是指编译器和处理器为了优化程序性能而对指令序列进重 新排序 的一种手段。即只要程序的最终结果与它顺序化情况的结果相等,那么 指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序 。
实例化对象其实可以分为三个步骤:
分配内存空间。初始化对象。将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能变为如下过程:
分配内存空间。将内存空间的地址赋值给对应的引用。初始化对象。
那么发生指令重排序的话,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤。线程B判断SINGLETONLANHAN已经不为空,获取到未初始化的SINGLETONLANHAN对象,那么就会报NullPointerException
使用volatile可以防止重排序,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换;volatile还可以保证其内存可见性。
最终代码如下:
private static volatile SingletonLanHan SINGLETonLANHAN =null;
public static SingletonLanHan getSingLetonLanHan() {
if (SINGLETonLANHAN == null) {
synchronized (SingletonLanHan.class) {
if (SINGLETonLANHAN == null) {
SINGLETonLANHAN = new SingletonLanHan();
}
}
}
return SINGLETONLANHAN;
}
4、饿汉模式(线程安全)
public class SingletonEHan {
private static SingletonEHan INSTANCE = new SingletonEHan();
private SingletonEHan(){}
public static SingletonEHan getInstance(){
return INSTANCE;
}
}
饿汉式单例在类加载初始化时,就创建好一个静态的对象供外部使用,除非系统重启,否则这个对象不会改变,所以本身是线程安全的。Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机内,Singleton的唯一实例只能通过geInstance()方法访问。(不过通过反射机制是能够实例化构造函数为private的类的,会使Java单例实现失效)
饿汉式的原则- 私有化该类的构造函数创建好一个静态的对象供外部使用定义一个公有的方法,将在该类中所创建的对象返回
在类加载初始化时就完成了实例化,避免了线程的同步问题
缺点就像你打王者,可能你地图还没打开,但是程序就已经将这些地图全都实例化了,到你手机上最明显体验就是一开游戏内存就满了,手机卡了,需要换了。官方语言(由于类加载时就实例化了,所以没有达到Lazy Loading(懒加载)的效果,也就是说可能我没有使用这个实例,但是 它也会加载,会造成内存浪费)
5、使用类的内部类(线程安全)public class SingletonEHan {
private SingletonEHan(){}
private static class SingletonHolder {
private static SingletonEHan instance = new SingletonEHan();
}
public static SingletonEHan getInnerInstance() {
return SingletonHolder.instance;
}
}
使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。此种方式也是非常推荐使用的一种单例模式 6、如何使用反射破坏单例
即使再完美的懒汉式或者饿汉式都敌不过反射,使用反射,强制访问类的私有构造器,去创建另一个对象
@Test
void SingletonReflectTest() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
// 获取类的构造器
Constructor construct = SingletonLanHan.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
SingletonLanHan obj1 = construct.newInstance();
// 通过正常方式获取单例对象
SingletonLanHan obj2 = SingletonLanHan.getInstance();
System.out.println(obj1 == obj2);
}
7. Effective Java作者推荐的枚举单例(线程安全)
在JDK1.5后,实现单例模式的方法又多出一种:枚举
让我们一起看看用枚举如何实现单例模式
public enum SingletonEnum {
INSTANCE;
public void test(){
System.out.println("Hello World!");
}
}
那枚举究竟有什么用呢?
看一下反射newInstance的源码,我们可以发现,它会判断该类是否是一个枚举类,如果是,那么它会抛出异常,所以它具备防止反射破坏的能力。
然后我们写一个测试案例,分析一下,他是否是单例,是否线程安全
@Test
void SingletonEnumTest() {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
//只输出一次创建对象
System.out.println(instance1==instance2);//true
SingletonEnum.INSTANCE.test();//Hello World!
}
由此可见,在程序启动时,会调用SingletonEnumTest的空参构造器,实例化好一个对象赋值给INSTANCE,之后再也不会实例化。
四、总结单例模式常见写法有两种:饿汉式和懒汉式懒汉式:默认不会实例化,在真正需要使用对象时才去创建该单例类对象;使用双重锁校验,解决并发安全和性能安全,对内存要求高的情况,最好使用懒汉式。饿汉式:在类加载时已经创建好该单例类对象,等待被程序使用;对内存要求不高的情况下,建议使用饿汉式,不考虑并发安全问题,且写法简单。在多线程环境下,因为指令重排序的导致变量报NullPointerException,需要再单例对象上加上volatile关键字防止重排序。那么使用枚举来实现单例的话,即可以让代码精简,又没有线程安全问题,且Enum类内部防止反射。 五、源码地址
演示代码
六、参考文献重学 Java 设计模式:实战单例模式「7种单例模式案例,Effective Java 作者推荐枚举单例模式」



