之所以被称为单例模式,是因为整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。具有以下特点
- 类构造器私有
- 持有自己类型的属性
- 对外提供获取实例的静态方法
静态内部类
当外部类Holder被加载时,内部类不会被加载。只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化instance ,其唯一性和线程安全性都由JVM保证。
package single;
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private final static Holder HOLDER = new Holder();
}
}
饿汉式
在类加载的同时创建好一个静态的对象供系统使用,其唯一性和线程安全性都由JVM保证。不过这种方法可能占用过多内存。
package single;
public class Hungry {
private Hungry() {
}
private final static Hungry HUNGRY = new Hungry(); // 在类加载是被创建,所以只有一个
public static Hungry getHungry() {
return HUNGRY;
}
}
懒汉式
需要时再去创建对象
package single;
public class Lazy {
private Lazy() {
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) { // 判断没有创建过时在创建,所以只有一个(单线程下)
lazy = new Lazy();
}
return lazy;
}
}
这个程序在多线程下很有可能产生问题,执行如下程序则可发现对象被创建了多次
package single;
public class Lazy {
private Lazy() {
// 如果输出则说明当前线程调用了构造方法,即new了一个对象
System.out.println(Thread.currentThread().getName() + "finish");
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
/* 输出
Thread-0finish
Thread-2finish
Thread-1finish
*/
双重检测锁模式 (DCL懒汉)
package single;
public class Lazy {
private Lazy() {
// 如果输出则说明当前线程调用了构造方法,即new了一个对象
System.out.println(Thread.currentThread().getName() + "finish");
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
这个程序在并发下仍然可能存在问题,因为new Lazy()
并不是一个原子操作(对象加载)
- 分配内存空间并初始化零值
- 初始化
- 将对象指向这个空间(不属于对象加载)
如果按照我们期望的1 -> 2 -> 3这个顺序的话,是不会有问题的,但是如果发生指令重排...
假设有一线程A,其执行顺序是 1 -> 3 -> 2;此时又进来一个线程B,当B判断if (lazy == null)
时,结果将会是false
,于是它就会直接返回lazy。如果你不理解为什么为false
,可以想一下:线程A先分配了内存空间,之后又把对象指向这个空间,也就是说,现在对象只是被初始化零值但是并没有初始化,但是注意,这时lazy已经不为空了!所以B线程进入的时候会以为lazy已经被创建好,所以直接返回。
要解决这个问题,需使用volatile
修饰lazy
实例变量
package single;
public class Lazy {
private Lazy() {
// 如果输出则说明当前线程调用了构造方法,即new了一个对象
System.out.println(Thread.currentThread().getName() + "finish");
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
但是这个方法依旧是不安全的,使用反射可以对其进行破坏。
简单解释一下为什么反射可以破坏这个单例:正常的通过getInstance
返回的对象是lazy = new Lazy()
出来的,那么显然,new出来的对象被指向了lazy也就是指向了一块内存空间,那么此时lazy不为空;
反观通过反射创建的对象,是直接通过获取构造器创建对象,并不通过getInstance
方法,那也就是没有检验lazy是否为空,所以可以再次创建一个新的实例。
package single;
import java.lang.reflect.Constructor;
public class Lazy {
private Lazy() {
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Lazy instance = Lazy.getInstance();
Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
declaredConstructor.setAccessible(true); // 破坏私有
Lazy instance2 = declaredConstructor.newInstance(); // 新建实例
System.out.println(instance);
System.out.println(instance2);
}
}
解决这种破坏方式,在构造器里添加检验。
package single;
import java.lang.reflect.Constructor;
public class Lazy {
private Lazy() {
synchronized (Lazy.class) {
if (lazy != null) {
throw new RuntimeException("请不要用反射破坏单例");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Lazy instance = Lazy.getInstance();
Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
declaredConstructor.setAccessible(true); // 破坏私有
Lazy instance2 = declaredConstructor.newInstance(); // 新建实例
System.out.println(instance);
System.out.println(instance2);
}
}
但是通过反射可以再次破坏,原因是如果第一次创建就采用反射创建,而反射直接调用构造器创建,并不会把创建的对象指向lazy,那么lazy始终为null。因此再创建第二个实例的时候也会成功。
package single;
import java.lang.reflect.Constructor;
public class Lazy {
private Lazy() {
synchronized (Lazy.class) {
if (lazy != null) {
throw new RuntimeException("请不要用反射破坏单例");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
declaredConstructor.setAccessible(true); // 破坏私有
Lazy instance = declaredConstructor.newInstance();
Lazy instance2 = declaredConstructor.newInstance(); // 新建实例
System.out.println(instance);
System.out.println(instance2);
}
}
我们可以通过添加一个标志位来解决这个问题
package single;
import java.lang.reflect.Constructor;
public class Lazy {
private static boolean secret = false;
private Lazy() {
synchronized (Lazy.class) {
if (secret == false) {
secret = true;
}
else {
throw new RuntimeException("请不要用反射破坏单例");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
declaredConstructor.setAccessible(true); // 破坏私有
Lazy instance = declaredConstructor.newInstance();
Lazy instance2 = declaredConstructor.newInstance(); // 新建实例
System.out.println(instance);
System.out.println(instance2);
}
}
但是这种写法其实还是不安全的,如果我们知道这个标识位的名字,我们还是可以通过使用反射进行破坏。在创建完实例后手动将标志位再设为false。
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Lazy {
private static boolean secret = false;
private Lazy() {
synchronized (Lazy.class) {
if (secret == false) {
secret = true;
}
else {
throw new RuntimeException("请不要用反射破坏单例");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
declaredConstructor.setAccessible(true); // 破坏私有
Lazy instance = declaredConstructor.newInstance();
Field secret = Lazy.class.getDeclaredField("secret");
secret.setAccessible(true);
secret.set(instance, false);
Lazy instance2 = declaredConstructor.newInstance(); // 新建实例
System.out.println(instance);
System.out.println(instance2);
}
}
可能现在你就想问了,如何写才能使安全的呢?其实枚举是无法被反射破坏的(点击new Instacne()
查看源码即知)。
枚举
package single;
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class TestEnumSingle {
public static void main(String[] args) {
EnumSingle instance = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
现在我们来确认一下枚举是否真的不能被破坏。
首先去out文件夹下找到编译后的文件,发现其中有一个无参构造方法。
package single;
public enum EnumSingle {
INSTANCE;
private EnumSingle() {
}
public EnumSingle getInstance() {
return INSTANCE;
}
}
于是我们开心的通过反射进行破坏
package single;
import java.lang.reflect.Constructor;
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class TestEnumSingle {
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.INSTANCE;
System.out.println(instance);
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
但是我们发现报错了,但是并不是我们期望的Cannot reflectively create enum objects
,而是Exception in thread "main" java.lang.NoSuchMethodException: single.EnumSingle.
。明明编译后的文件里有无参构造器,为什么通过反射创建时又说没有呢?我们可以自己反编译查看一下,发现确实是有无参构造方法的...
javap -p EnumSingle
这样一来,我们就需要更专业的工具了-jad,通过jad反编译生成.java文件然后查看:
jad -sjava EnumSingle.class
这次我们发现,终于不是无参构造器了
我们修改一下getDeclaredConstructor
,然后再次尝试破坏
package single;
import java.lang.reflect.Constructor;
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class TestEnumSingle {
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.INSTANCE;
System.out.println(instance);
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
这次报错Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
,是我们想要的,说明枚举无法被反射破坏。
如果想更深入的理解单例模式,可以看这片文章-传送门