单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例(构造器私有)
- 单例类必须给所有其他对象提供这一实例
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
- 避免对资源的多重占用(比如写文件操作)
缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
单例模式主要分为饿汉式和懒汉式两种,下面对这两种方式做一下介绍
一、饿汉式
饿汉式,根据表面意思,就是他很饿。表现出来就是在类加载时就完成了初始化。
public class singleton {
// 直接初始化对象
private final static singleton single = new singleton();
private singleton() {
} //构造器私有
public static singleton getInstance() {
return single;
}
}
饿汉式避免了多线程的同步问题,但是不管后面用不用得上,在类加载时直接初始化,没有达到Lazy Loading (懒加载) 的效果,极有可能造成内存浪费。
二、懒汉式
懒汉式就是在类加载时先不初始化,等到第一次被使用时才初始化。
// 线程不安全
public class singleton {
private static singleton single;
private singleton() {
}
public static singleton getInstance() {
if(single==null) {
//不为null说明已经初始化过了,不需要再进行初始化
single = new singleton();
}
return single;
}
}
懒汉式不会造成内存浪费。上面的代码在单线程下没有问题,但是在多线程下极有可能出现问题。
类加载是有顺序的,主要分为 加载 —> 连接 —> 初始化。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果。
详细类加载过程可以看下面这篇文章
(二)浅谈JVM的类加载知识
懒汉式的多线程安全问题可以使用DCL解决:
DCL懒汉式(双重检测锁模式)
就是在实例化前加锁,防止并发情况下出现多次实例化
public class singleton {
private static singleton single;
private singleton() {
}
public static singleton getInstance() {
if(single==null) {
synchronized (singleton.class) {
if(single == null)
single = new singleton();
}
}
return single;
}
}
进行两次 if (singleton == null)检查,这样可以保证线程安全,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
DCL虽然保证了线程安全,但是又会出现另一个问题:不能保证new过程的原子性操作
new过程主要有三步:
- 1、分配内存空间
- 2、执行构造方法、初始化对象
- 3、把对象指向空间
正常的顺序是123,但是也有可能出现132的情况,就是还没初始化先指向内存空间。这时候如果有两个线程同时访问,第一个线程不会有问题,会造成第二个线程查询到 single 不为空,直接返回 single,但是此时 single 还没有完成初始化,是个空对象,会出现问题。这个解决办法就是加上 volatile
关键字。
public class singleton {
// 加上volatile关键字,保证按照123顺序执行,使其成为原子性操作
private volatile static singleton single;
private singleton() {
}
public static singleton getInstance() {
if(single==null) {
synchronized (singleton.class) {
if(single == null)
single = new singleton();
}
}
return single;
}
}
volatile 关键字的主要作用是保证变量的可见性并且防止指令重排序。
三、如何破坏单例模式
单例模式其实是可以破坏的,用的方式就是Java的反射机制
1、调用原始构造和反射构造情况下
import java.lang.reflect.Constructor;
public class singleton {
private volatile static singleton single;
private singleton() {
} // 无参构造方法
public static singleton getInstance() {
if(single==null) {
synchronized (singleton.class) {
if(single == null)
single = new singleton();
}
}
return single;
}
public static void main(String args[]) throws Exception{
singleton instance1 = single.getInstance();
// 获取空参构造器,调用无参构造方法
Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
// 破坏私有构造器,无视私有化
deConstructor.setAccessible(true);
// 通过反射创建对象
singleton instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
输出如下:
结果发现两个实例地址不一样,本来单例只有一个实例,两个应该相同,结果却不一样,说明反射已经破坏了单例模式
如何防止被反射破坏
简单的就是在无参构造器里加锁
import java.lang.reflect.Constructor;
public class singleton {
private volatile static singleton single;
private singleton() {
synchronized (singleton.class) {
if(single!=null)
throw new RuntimeException("不要试图使用反射破坏单例模式");
}
}
public static singleton getInstance() {
if(single==null) {
synchronized (singleton.class) {
if(single == null)
single = new singleton();
}
}
return single;
}
public static void main(String args[]) throws Exception{
singleton instance1 = single.getInstance();
Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
deConstructor.setAccessible(true);
singleton instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
2、都调用反射构造
如果不调用原始的getInstance()
方法,都调用反射创建的方法,也会造成问题,例如
import java.lang.reflect.Constructor;
public class singleton {
private volatile static singleton single;
private singleton() {
} // 无参构造方法
public static singleton getInstance() {
if(single==null) {
synchronized (singleton.class) {
if(single == null)
single = new singleton();
}
}
return single;
}
public static void main(String args[]) throws Exception{
// 获取空参构造器,调用无参构造方法
Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
// 无视私有构造器
deConstructor.setAccessible(true);
// 通过反射创建对象
singleton instance1 = deConstructor.newInstance();
singleton instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
// 输出(两个值还是不一样)
// forTest.singleton@2ff4acd0
// forTest.singleton@54bedef2
综上,通过反射很容易破坏单例模式,那么如何解决呢?
下面介绍终极解决办法:
四、如何防止单例模式被反射破坏
因为反射不能破坏枚举类型,所以通过枚举可以防止单例模式被破坏。如果有反射去破坏枚举,程序会直接报错:java.lang.IllegalArgumentException: Cannot reflectively create enum objects(不能使用反射机制创建一个枚举对象)。
import java.lang.reflect.Constructor;
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class test{
public static void main(String []args) throws Exception{
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> deConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
deConstructor.setAccessible(true);
EnumSingle instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
// 输出:
// java.lang.IllegalArgumentException: Cannot reflectively create enum objects
由于枚举类不能在外部实例化对象,并且无偿提供了序列化机制,绝对防止了多次实例化。单元素枚举已经成为实现Singleton的最佳方式。
注意:Constructor<EnumSingle> deConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
这里使用的是有参构造器,因为枚举编译后其实不存在无参构造器,源码里面看是有无参构造,但是编译后是有参构造,具体可以通过jad.exe查看反编译后的文件(如下图)
本文来源:(以上看不懂的,或者枚举那里不懂的,强烈推荐看下面这个视频)
【狂神说Java】单例模式-23种设计模式系列