什么是单例模式?

保证类在内存中只能有一个对象,且构造私有。

主要解决: 一个全局使用的类频繁地创建与销毁。

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

饿汉模式

】在类加载就创建了实例,

优点】多线程安全,在类一加载就初始化了实例

缺点】资源浪费

package com.singleton;

/**
 * 饿汉模式
 * 在类一加载的时候就创建了实列
 * 缺点是,会出现资源浪费
 */
public class Hungry {
    //资源浪费了
    Byte[] byte1 = new Byte[1024];
    Byte[] byte2 = new Byte[1024];
    Byte[] byte3 = new Byte[1024];
    Byte[] byte4 = new Byte[1024];

   private Hungry(){

   }

   private static Hungry hungry = new Hungry();

   public static Hungry getInstance(){
       return hungry;
   }
}

】这个单例是不安全的,我们可以用反射破坏这个单例。

使用反射破坏

class test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

Constructor<Hungry> declaredConstructor = Hungry.class.getDeclaredConstructor(null);
        //为true 表示可以获得私有成员
        declaredConstructor.setAccessible(true); 
         //没通过这个单例类u getInstance()方法 就创建了实例。
        Hungry hungry = declaredConstructor.newInstance();
    }
}

懒汉模式与线程安全懒汉模式

】当需要的时候才会创建类的实例

优点】多线程不安全,加上synchronized使多线程下安全

缺点】必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)

package com.singleton;

/**
 * 当要使用的时候才创建实例
 */
public class Lazy {
     private Lazy(){
        System.out.println("线程"+Thread.currentThread().getName()+"创建了一个实例");
    }
    private static Lazy lazy ;

    //没有synchronized会出现线程安全问题,所以在方法上加上synchronized就线程安全了
    public static   Lazy getInstance(){
        if(lazy == null){
            lazy = new Lazy();
        }
        return  lazy;
    }
}

模拟多线程不安全

class test2{
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                Lazy.getInstance();
            },i+"").start();
        }
    }
}
/*输出:
线程1创建了一个实例
线程3创建了一个实例
线程4创建了一个实例
线程2创建了一个实例
线程0创建了一个实例
*/

为什么会出现这种情况?

指令重排,并且创建实例不是一个原子性操作。

创建实例的顺序:

1、分配内存空间

2、执行构造方法、初始化对象

3、把这对象指向这个内存空间

到了多线程,就会出现——A线程还没执行完,B线程来了发现为null继续创建对象。

】依然可以用反射来破坏这种单例

使用反射破坏

class test2{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

   Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        //为true 表示可以获得私有成员
        declaredConstructor.setAccessible(true); 
        //没通过这个单例 getInstance()方法 就获得了实例。
        Lazy lazy = declaredConstructor.newInstance();

    }
}

DCL懒汉模式

什么是DCL?

DCL也叫 双检锁/双重校验锁(DCL,即 double-checked locking)。

也就是说我们给这个单例上双重检测,并且加上锁。防止多线程的不安全发生。

优点】多线程安全

package com.singleton;

public class DCLLazy {
    private DCLLazy() {}
    //volatile防止指令重排,synchronized可以保证原子性
    private static volatile DCLLazy dclLazy = null;

    public static DCLLazy getInstance(){
        //如果这个实例不为空了其他线程都不能进来,还可以防止线程重复拿到锁,浪费资源
        if (dclLazy == null) {
            //当第一个线程进入后会给这个类上锁
            synchronized (DCLLazy.class) {
                if (dclLazy == null) {
                    dclLazy = new  DCLLazy();
                }
            }
        }
       return  dclLazy;
    }
}

】还是不能防止反射破坏这个单例

使用反射破坏

class  test3{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

Constructor<DCLLazy> declaredConstructor = DCLLazy.class.getDeclaredConstructor(null);
       DCLLazy dclLazy = declaredConstructor.newInstance();
       DCLLazy dclLazy2 = declaredConstructor.newInstance();
       System.out.println(dclLazy.equals(dclLazy2)); //输出false ,说明不是同一个对象
    }
}

那么我们给这个DCL加强下,在构造加上

 private DCLLazy() {
           synchronized (DCLLazy.class) { //加锁,保证多线程下原子性
            if (judge == false) { //第一次创建实例为false,并修改judge为true
                judge = true;
            } else {
                throw new RuntimeException("不要使用反射来破坏");
            }
        }
    }

再次运行main方法,发现报错了,控制台出现了我们抛出了异常,那是不是代表解决了呢?并不是,反射能获取私有的属性并且改变值。

Caused by: java.lang.RuntimeException: 不要使用反射来破坏
    at com.singleton.DCLLazy.<init>(DCLLazy.java:15)
    ... 6 more

修改反射代码:

class test3 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Field judge = DCLLazy.class.getDeclaredField("judge"); //获得属性
        judge.setAccessible(true);
Constructor<DCLLazy> declaredConstructor =DCLLazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //创建了第一个实例
        DCLLazy dclLazy = declaredConstructor.newInstance(); 

        //出现把judge属性设为false,这样就能够继续创建实例了
        judge.set(declaredConstructor,false); 

        DCLLazy dclLazy2 = declaredConstructor.newInstance();
        //依然输出false ,说明依然不是同一个对象
        System.out.println(dclLazy.equals(dclLazy2)); 
    }
}

静态内部类

实现了懒加载,只有当内部类被调用了,才会被类加载器加载。

优点】多线程安全,这种方式能达到双检锁方式一样的功效,但实现更简单

package com.singleton;

public class StaticSingleton {
    private StaticSingleton(){}

    public StaticSingleton getInstance(){
        return instanceClass.STATIC_SINGLETON;
    }

    //静态内部类
    public static class instanceClass{
   private  static final  StaticSingleton STATIC_SINGLETON = new StaticSingleton();
    }
}

】依然能用反射破坏这个单例

反射破坏

class test4{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<StaticSingleton> declaredConstructor = StaticSingleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true); //为true 表示可以获得私有成员
        StaticSingleton ss = declaredConstructor.newInstance(); //没通过这个单例 getInstance()方法 就获得了实例。
    }
}

枚举

优点】这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,多线程安全。

描述: 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

】可以防止反射。

public enum  EnumSingleton {
    INSTANCE;
     public void outPut(){
        System.out.println("红火火恍恍惚惚");
    }
}

我们来看看编译后的class文件:

发现被编译后的class文件有一个私有的构造方法,那我们试试用反射能不能调用到这个构造。

package li;

public enum EnumSingleton {
    INSTANCE;

    private EnumSingleton() {
    }

    public void outPut() {
        System.out.println("红火火恍恍惚惚");
    }
}

使用反射尝试破坏

class test5{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingleton enumSingleton = declaredConstructor.newInstance();
        enumSingleton.outPut();
    }
}

发现报错了,告诉我们,没有这个构造方法。

Exception in thread "main" java.lang.NoSuchMethodException: li.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at li.test5.main(EnumSingleton.java:15)

我们使用jad进行反编译,先要打开jad,然后在cmd输入:jad -sjava EnumSingleton.class

然后出现了一个java后缀的源文件:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java

package li;

import java.io.PrintStream;

public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
    }

    //看这里,发现是有两个参数的有参构造方法
    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public void outPut()
    {
        System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

那我们用反射看能不能使用这个有参构造

class test5{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //使用有参构造
 Constructor<EnumSingleton> declaredConstructor =           EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);

        EnumSingleton enumSingleton = declaredConstructor.newInstance();
        enumSingleton.outPut();
    }
}

抛出了异常:告诉你不能使用反射来创建枚举对象

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at li.test5.main(EnumSingleton.java:17)

总结

一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。