前言
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式常见分为懒汉式和饿汉式,具体介绍参考菜鸟教程
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
一. 饿汉式
饿汉式的特点
- 初始化直接创建对象
- 如果创建大量不使用的对象就是资源的浪费
- 饿汉式不会出现线程问题
- 反射可能会出现非单例问题
如果不了解并发下懒汉式和饿汉式,直接饿汉式就可以了,不会出现并发问题
代码实现饿汉式
package com.concurrent.demo20Single;
/**
* 饿汉式
* 1. 直接创建对象
* 2. 如果创建大量不使用的对象就是资源的浪费
* 3. 饿汉式不会出现线程问题
* 4. 反射可能会出现非单例问题
* @author lane
* @date 2021年05月29日 下午4:44
*/
public class SingleDemoHungry {
// 这就是浪费空间的代码
/*private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];*/
public static SingleDemoHungry singleDemoHungry = new SingleDemoHungry();
private SingleDemoHungry(){};
public static SingleDemoHungry getInstance(){
return singleDemoHungry;
}
}
饿汉式测试
package com.concurrent.demo20Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author lane
* @date 2021年05月29日 下午4:48
*/
public class SingleTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
hungryTest();
}
//测试饿汉式
public static void hungryTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//正常测试懒汉式单例
SingleDemoHungry singleDemoHungry1 = SingleDemoHungry.getInstance();
SingleDemoHungry singleDemoHungry2 = SingleDemoHungry.getInstance();
System.out.println("饿汉式测试是否单例:"+(singleDemoHungry1==singleDemoHungry2));//true
//反射测试懒汉
Class<SingleDemoHungry> singleDemoHungryClass = SingleDemoHungry.class;
Constructor<SingleDemoHungry> constructor = singleDemoHungryClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingleDemoHungry singleDemoHungry11 = constructor.newInstance();
System.out.println("饿汉式测试反射是否单例:"+(singleDemoHungry11==singleDemoHungry1));//false
}
}
//打印结果
饿汉式测试是否单例:true
饿汉式测试反射是否单例:false
结果表明了不使用反射情况下可以实现单例效果,但是反射创建对象的话无法保证单例
想要保证单例只需要修改下构造方法为如下即可
private SingleDemoHungry(){
if (singleDemoHungry!=null){
throw new RuntimeException("小老弟,没想到吧,防御一手");
}
}
二. 懒汉式
懒汉式特点
- 需要的时候才创建
- 多线程会出现问题
- 资源不会像饿汉那样浪费
- 反射仍会出现问题需要防御两手
代码实现懒汉式
package com.concurrent.demo20Single;
/**
* 饿汉式
* 为了线程安全需要加volatile和synchronized
* @author lane
* @date 2021年05月29日 下午10:53
*/
public class LazyDemo {
private static volatile LazyDemo lazyDemo ;
private LazyDemo(){}
/**
* lazyDemo = new LazyDemo();非原子性操作
* 1. 分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 123 单线程正常 123执行
* 132 指令重排 变成 132 执行
* 多线程 A 指令重排执行到13的时候 ,多线程B进入第一个判断发现已经分配空间
* 开始执行return 并继续操作,实际对象并没有初始化,出现问题
* 所以需要加上volatile关键字来避免指令重排
* DCL懒汉式 就是双重加锁检测
*/
public static LazyDemo getInstance(){
if (lazyDemo==null){
synchronized (LazyDemo.class) {
if (lazyDemo ==null){
//非原子性操作
lazyDemo = new LazyDemo();}
}
}
return lazyDemo;
}
}
对于添加volatile关键字的解读
在 private static volatile LazyDemo lazyDemo ;
这行代码上为什么加上volatile的解读
lazyDemo = new LazyDemo();
这行代码并非原子性操作
-
分配内存空间
-
执行构造方法,初始化对象
-
把这个对象指向这个空间
假如在下面这种情况下会出现问题
-
123 单线程正常 123执行
-
132 指令重排 变成 132 执行
-
多线程 A 指令重排执行到13的时候 ,多线程B进入第一个判断是否为null 发现已经分配空间
-
开始执行return 并继续操作,实际对象并没有初始化,出现问题
需要加上volatile关键字来避免指令重排
Volatile 可以保证可见行和避免指令重排,但是不能保证原子性
懒汉式测试
package com.concurrent.demo20Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author lane
* @date 2021年05月29日 下午4:48
*/
public class SingleTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
// hungryTest();
LazyTest();
}
//懒汉式测试
private static void LazyTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyDemo lazyDemo1 =LazyDemo.getInstance();
LazyDemo lazyDemo2 =LazyDemo.getInstance();
System.out.println("懒汉式测试是否单例:"+(lazyDemo1==lazyDemo2));//true
//反射创建
Class<LazyDemo> lazyDemoClass = LazyDemo.class;
//反射获取无参构造器
Constructor<LazyDemo> declaredConstructor = lazyDemoClass.getDeclaredConstructor(null);
//允许访问私有构造
declaredConstructor.setAccessible(true);
LazyDemo lazyDemo11 = declaredConstructor.newInstance();
System.out.println("懒汉式测试反射是否单例:"+(lazyDemo1==lazyDemo11));//false
}
}
测试结果
发现懒汉式单例在反射依然不够安全,需要修改一下构造函数
private LazyDemo(){
if(lazyDemo!=null){
throw new RuntimeException("懒汉式防御第一手");
}
}
测试结果
反射再修改一下也会导致懒汉式再次变得不安全
LazyDemo lazyDemo11 = declaredConstructor.newInstance();
LazyDemo lazyDemo12 = declaredConstructor.newInstance();
System.out.println("懒汉式测试反射是否单例:"+(lazyDemo11==lazyDemo12));//false
测试结果
构造函数再修改下避免这种反射问题
private static int num = 0;
private LazyDemo(){
if(lazyDemo!=null){
throw new RuntimeException("懒汉式防御第一手");
}
if (num==0){
num =1;
}else {
throw new RuntimeException("懒汉式防御第二手");
}
}
再次测试结果
以上基本就可以保证单例模式了,如果攻击者知道我们定义的字段是num的话,每次修改一下num的初始值也是无法保证单例的。
//除非知道字段否名称,否则无法再次破坏了
Field num = lazyDemoClass.getDeclaredField("num");
LazyDemo lazyDemo11 = declaredConstructor.newInstance();
num.set(lazyDemo11,0);
LazyDemo lazyDemo12 = declaredConstructor.newInstance();
System.out.println("懒汉式测试反射是否单例:"+(lazyDemo11==lazyDemo12));//false
官方对于反射,有完全安全的方式就是枚举类
三. 静态内部类单例模式
package com.concurrent.demo20Single;
/**
* 静态内部类创建单例模式
* @author lane
* @date 2021年05月30日 上午12:02
*/
public class InnerDemo {
private InnerDemo(){
// System.out.println(Thread.currentThread().getName()+"单例创建");
}
public static InnerDemo getInstance(){
return InnerClass.innerDemo;
}
//静态内部类
private static class InnerClass {
private static final InnerDemo innerDemo = new InnerDemo();
}
/* public static void main(String[] args) {
//测试下线程是否安全的 //Thread-0单例创建
for (int i = 0; i < 10; i++) {
new Thread(()->{
InnerDemo innerDemo = InnerDemo.getInstance();
}).start();
}
InnerDemo innerDemo1 = InnerDemo.getInstance();
InnerDemo innerDemo2 = InnerDemo.getInstance();
System.out.println(innerDemo1 == innerDemo2);
}*/
}
四. 枚举类单例模式
枚举类不会出现线程问题,反射也无法破坏枚举类
代码实现
package com.concurrent.demo21enum;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 枚举单例
* @author lane
* @date 2021年05月31日 下午6:11
*/
public enum Single {
INS;
private Single (){}
public static Single getInstance(){
return INS;
}
}
class EnumDemo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Single instance1 = Single.getInstance();
Single instance2 = Single.getInstance();
System.out.println("枚举是否单例" +(instance1 == instance2));
Class<Single> singleClass = Single.class;
//因为枚举默认继承Enum类,必须两个参数的构造
// singleClass.getDeclaredConstructor(null);
Constructor<Single> declaredConstructor = singleClass.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Single single1 = declaredConstructor.newInstance();
Single single2 = declaredConstructor.newInstance();
System.out.println("反射下看枚举单例能否被破坏:"+(single1==single2));
}
}
测试结果
源码介绍