单例模式
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
注意:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
单例模式中最重要的思想:构造器私有(private),一旦构造器私有,就无法用new关键字去创建对象了。
接下来,介绍几种单例模式的写法
1. 饿汉式
package com.singleton;
//饿汉式单例
public class Hungry {
private byte[] data= new byte[1024*1024];
private Hungry(){}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉式有什么问题呢?
正如代码中所示,饿汉式会浪费内存,我们在类中定义了一个数组data,当使用饿汉式单例时,会将这些数组对象都加载进入内存,但是对象空间都没有使用。造成空间浪费。饿汉式的优点是:没有线程安全的问题
2. 懒汉式
package com.singleton;
public class Lazy {
private Lazy(){}
private static Lazy instance;
public static Lazy getInstance(){
if(instance == null){
instance = new Lazy();
}
return instance;
}
}
懒汉式就是在用到实例的时候才去创建,并且创建的时候进行检查,如果没有实例化则创建新对象,否则直接返回对象,不再创建新对象。
懒汉式在单线程下,运行很好;但是在多线程并发下,会出现问题
2.1 多线程测试懒汉式
package com.singleton;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+" start...");
}
private static Lazy instance;
public static Lazy getInstance(){
if(instance == null){
instance = new Lazy();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
instance.getInstance();
}).start();
}
}
}
多次运行输出结果:
3. 双重检测锁模式(DCL)
package com.singleton;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+" start...");
}
private volatile static Lazy instance;
public static Lazy getInstance(){
//加锁之前,可能会被多个线程拿到,所以判断两次
if(instance == null){//1
synchronized (Lazy.class){
if(instance == null){//2
instance = new Lazy();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
instance.getInstance();
}).start();
}
}
}
判断两次解释?
假如A,B两个线程同时执行到代码1处,然后两个线程会竞争锁。假如A线程获得了锁,继续执行,直到创建一个Lazy实例对象,然后A线程释放锁。接着B线程获取锁,继续执行,在代码2处再次检查instance是否为null。由于synchronized可以保证线程可见性,所以不会再重新创建一个新的实例对象。但是,如果没有代码2的判断,线程B就会创建另一个Lazy对象,破坏单例模式。
注意:instance变量 要加上 volatile 关键字,那么为什么需要 volatile 关键字呢?
由于代码 instance=new Lazy();不是一个原子性操作,这行代码需要三个步骤:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
由于Java虚拟机中的 指令重排 可能导致上面三个步骤并不是顺序执行,假设线程A执行顺序为132,当执行到3时,表示已经占据了内存空间,只是还没有进行初始化;如果此时线程B执行该方法,就会发现instance不为空,从而返回instance,但是实际上instance并没有初始化。
volatile 关键字有两个语义:一是保证变量的可见性,二是禁止指令重排。在这里,可见性由synchronized就可以保证,同时synchronized保证了原子性,因此DCL中volatile 的作用是禁止指令重排。
4. 静态内部类实现单例模式
package com.singleton;
//静态内部内实现
public class Single {
private Single(){}
public static Single getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Single HOLDER = new Single();
}
}
静态内部类的方式在效果上类似DCL,但实现更简单。但是只适用于静态域的情况,而DCL适用范围更大。
5. 枚举
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
枚举较为少见,它支持序列化,并且绝对防止多次实例化。
6. 扩展
问题:DCL真的能保证单例模式安全吗?
众所周知,在java中有一个bug般的存在:反射。由于反射存在,任何private都是浮云,接下来使用反射机制破坏DCL。
6.1 反射破坏DCL
对DCL进行测试,修改main方法
package com.singleton;
import java.lang.reflect.Constructor;
public class Lazy {
private Lazy(){}
private static Lazy instance;
public static Lazy getInstance(){
//加锁之前,可能会被多个线程拿到,所以判断两次
if(instance == null){
synchronized (Lazy.class){
if(instance == null){
instance = new Lazy();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception {
//正常情况下创建对象
Lazy instance1 = Lazy.getInstance();
//反射创建对象 获取其空参构造器
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
//设置权限,无视私有(private)关键字
constructor.setAccessible(true);
//通过构造器创建对象
Lazy instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1==instance2);
}
}
输出结果:
=======================================我是分割线====================================
我们发现程序最终创建了两个实例对象,可以得到结论,反射可以破坏单例。那么我们在构造器中再加一个锁能不能解决呢?试一下,修改构造器,其他代码不变
private Lazy(){
synchronized (Lazy.class){
if(instance != null){
throw new RuntimeException("不要试图使用反射搞破坏!");
}
}
}
输出结果:
=======================================我是分割线====================================
从上面结果可以看出,我们好像成功了,别急修改一下main方法,在测试一次
public static void main(String[] args) throws Exception {
//反射创建对象 1.获取其 空参构造器
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
//设置权限,无视私有(private)关键字
constructor.setAccessible(true);
//通过构造器创建对象
Lazy instance1 = constructor.newInstance();
Lazy instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1==instance2);
}
输出结果:
运行发现,没报错???
结论:在之前的代码中,我们是通过 Lazy instance1 = Lazy.getInstance() 创建了对象,所以才会使得instance不为空,而这里我们的两个对象都是使用反射创建的,结果就没报错了。
=======================================我是分割线====================================
我们再加入一个标志位能不能解决呢?当反射创建一个对象的时候,我们修改标志位,那么就不会再次创建。在上面的基础上,我们添加一个标志位,并且修改构造器
package com.singleton;
import java.lang.reflect.Constructor;
public class Lazy {
private static boolean flag = false;
private Lazy(){
synchronized (Lazy.class){
if(flag == false){
flag = true;
}else{
throw new RuntimeException("不要试图使用反射搞破坏!");
}
}
}
private static Lazy instance;
public static Lazy getInstance(){
//加锁之前,可能会被多个线程拿到,所以判断两次
if(instance == null){
synchronized (Lazy.class){
if(instance == null){
instance = new Lazy();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception {
//反射创建对象 1.获取其 空参构造器
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
//设置权限,无视私有(private)关键字
constructor.setAccessible(true);
//通过构造器创建对象
Lazy instance1 = constructor.newInstance();
Lazy instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1==instance2);
}
}
上面代码,除了新加一个flag并且修改构造器,其他什么都没有变!!
输出结果:
=======================================我是分割线====================================
通过标志位进行控制,在不使用反编译的情况下,是找不到我们定义的标志位的,并且标志位如果再进行一些加密处理,会使得反射变得更加安全。
既然要测试,那就测试到底!!!反射既然能得到构造器,那使用反射得到字段还不是一样,创建一个对象之后,使用反射修改标志位,然后创建另一个对象。修改main方法,继续测试:
public static void main(String[] args) throws Exception {
//反射获取标志位字段
Field flag = Lazy.class.getDeclaredField("flag");
//破坏其私有权限
flag.setAccessible(true);
//反射创建对象 1.获取其 空参构造器
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
//设置权限,无视私有(private)关键字
constructor.setAccessible(true);
//通过构造器创建对象
Lazy instance1 = constructor.newInstance();
//创建一个对象后,更改标志位,重新设置false
flag.set(instance1,false);
Lazy instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1==instance2);
}
输出结果:
单例再次被破坏!!!
这里可能有人问,若是不知道字段名呢,这里获取字段是直接传入一个参数flag,那如果不知道怎么办?
//反射获取标志位字段
Field[] flag = Lazy.class.getDeclaredFields();
for (Field field : flag) {
System.out.println(field);
}
输出结果:
是不是可以得到了呢
结论:无限套娃....
=======================================我是分割线====================================
那到底如何解决呢?我们先查看newInstance方法的源代码:
在上图的源码中,我们发现:如果类型是枚举类型,那么就不能反射破坏枚举,接下来,我们测试反射到底能不能破坏枚举。
6.2 反射和枚举
枚举实现单例在上面已经写过了,我们就使用上面代码进行测试
package com.singleton;
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> constructor = EnumSingle.class.getDeclaredConstructor(null);
//破坏其权限
constructor.setAccessible(true);
//得到对象
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
上述代码为:创建了一个测试类Test,并且使用反射获取枚举对象,看看是否能成功!!
输出结果:
从这里可以看出,结果抛出异常,但是这个异常是NoSuchMethodException,意思是没有空参的构造方法???是不是很奇怪,那么我们来查看一下编译后的class文件:
然而,class文件中明明存在空参构造方法呀!是不是IDEA的原因呢?反编译在试一下:
反编译之后,发现仍然存在空参构造方法,那为什么会报那样的错误呢?接下来,我们使用一个更加强大的反编译工具jad进行反编译试一下:
去查看生成的这个java文件:
查看这个文件,发现确实没有空参的构造方法,而是两个参数的构造方法,接下来,我们更改之前的测试类,通过有参构造器获取对象
class Test{
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
//得到其无参构造
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
//破坏其权限
constructor.setAccessible(true);
//得到对象
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
输出结果:
至此,我们得到了期望的异常,与newInstance方法中描述的一致。也即不能使用反射创建枚举对象。
结论:反射不能破坏枚举的单例模式,也对应了上面枚举中的那句话 枚举绝对防止多次实例化。