栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

设计模式——单例模式

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

设计模式——单例模式

单例模式

单例模式是指Java虚拟机中一个类只有一个实例。

饿汉式
// 1. 饿汉式
public class Singleton1 {
//私有构造方法
    private Singleton1() {
        System.out.println("private Singleton1()");
    }
//私有静态变量
    private static final Singleton1 INSTANCE = new Singleton1();
//公开获取对象的方法
    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

实现要点:

  1. 构造方法是私有的,不然其他类有机会调用构造方法来创造实例对象
  2. 提供一个静态的成员变量INSTANCE,成员变量的类型就是单例类型,值就是用私有构造方法创建出来的一个唯一实例。
  3. 因为静态变量一般是私有的不能直接访问,一般会提供公共的静态方法,方法名就叫getInstance()获得实例,实现是返回刚刚的成员变量。

所谓饿汉式是相对于懒汉式来说的,懒汉式是指当第一次调用getInstance()方法时才会去创建实例。而饿汉式则不是,只要类加载初始化了,这个实例就能被创建。

这里的otherMethod()方法是为了测试用的。在测试中先调用otherMethod(),这时候类就会被初始化,执行类初始化操作的时候就会导致

反射破坏单例及解决办法

反射直接调用私有的构造方法破坏单例。

reflection(Singleton1.class);


    private static void reflection(Class clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
//获取无参构造方法
        Constructor constructor = clazz.getDeclaredConstructor();
//使私有的构造方法也可以被使用
        constructor.setAccessible(true);
//调用构造方法的newInstance()
        System.out.println("反射创建实例:" + constructor.newInstance());
    }

可以预防这种情况的发生:在构造方法中做个判断

public class Singleton1 {
    private Singleton1() {
//增加判断解决反射破坏单例
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
    }
}
反序列化破坏单例

 一旦对象实现了反序列化接口----->implements Serializable------>就有可能被利用反序列化来破坏单例:因为反序列化创建对象是不走构造方法的

 // 反序列化破坏单例
        serializable(Singleton1.getInstance());


private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
//把对象变成字节流
        oos.writeObject(instance);
//再把字节流还原成一个实例
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:" + ois.readObject());
    }

预防的方法:在类中写一个方法readResolve(),在反序列化创建对象的时候如果发现了类中有重写这个方法,就会把这个方法的返回值作为结果返回

    public Object readResolve() {
        return INSTANCE;
    }
Unsafe 破坏单例

unsafe是JDK内置的一个类,也是不会走类的构造方法创建实例。

// Unsafe 破坏单例
        unsafe(Singleton1.class);
 

    private static void unsafe(Class clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
        System.out.println("Unsafe 创建实例:" + o);
    }
枚举饿汉式 

 只定义了一个变量INSTANCE,这样就控制了只有唯一的实例

// 2. 枚举饿汉式
public enum Singleton2 {
    INSTANCE;

}

枚举类不会被反序列化破坏单例,因为在反序列化中遇到的如果是枚举类会特殊处理,直接返回。

反射也不会破坏单例,如果反射中发现是枚举类会直接抛异常。

unsafe会破坏单例。

懒汉式单例 
// 3. 懒汉式单例
public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // Singleton3.class
    public static  Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

}

懒汉式单例是指当第一次调用getInstance()方法时才会去创建实例,那么在一开始的私有变量INSTANCE中就先不赋值,在调用getINSTANCE()的时候判断是否为null,如果是就创建,否则什么都不做。

但是在多线程的时候会出现并发安全问题。考虑如下情况:有两个线程,在判断INSTANCE==null中,此时为true,线程一准备去创建实例对象,这时候切换到线程二,因为线程一的对象创建还没完成,所以线程二的判断也为true,也进入到了对象的创建。解决办法:在getINSTANCE()方法加上synchronized关键字,就可以加上线程安全的保护。原理:加在静态方法上的synchronized就是相当于在singleton3.class上加了一把锁,当线程一运行到这个方法时发现是静态方法而且这里加了synchronized关键字,就会去尝试获取singleton3.class的锁,然后去运行;如果有其他线程来到了这个方法也要去判断是否能拿到这个锁。但是这样性能并不好,因为这种情况只有发生在第一次创建INSTANCE时才会出现,所以希望加锁是只针对第一次创建INSTANCE的情况。

 DCL懒汉式单例

对上一种懒汉式的优化 ,双检锁懒汉式。加锁之前先判断INSTANCE是否为null,如果不为null就直接返回了,如果为null,就进入synchronized方法

// 4. 懒汉式单例 - DCL
public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    private static volatile Singleton4 INSTANCE = null; // 可见性,有序性

    public static Singleton4 getInstance() {
        if (INSTANCE == null) {
//如果为null证明是第一次进入
            synchronized (Singleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

为什么在synchronized方法还要再次判断INSTANCE是否为null?假设没有这次判断,在竞争锁后线程一进入了代码块并创建出INSTANCE,释放锁,线程二获取锁之后也会再次创建INSTANCE。

为何要加volatile

 创建对象方法对应的Java底层指令是创建对象(计算需要多少空间内存等),调用构造(给成员变量赋值)、给instance赋值。CPU执行的时候可能会对指令的执行进行优化,调换指令执行的次序。因为给成员变量赋值和给instance赋值没有因果关系,所以执行的时候就有可能调换这两条指令的顺序,这种调换在单线程下没有影响,但是在多线程下就不一定了

  • INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造

  • 如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象

 加上volatile就是对修饰的变量的赋值语句之后加上一个内存屏障,阻止之前的赋值语句越过屏障

饿汉式的实例创建是放在静态代码块中实现,而静态代码块的线程安全JVM会保证,而懒汉式就要考虑,于是有了内部类懒汉式。

内部类懒汉式单例
// 5. 懒汉式单例 - 内部类
public class Singleton5 implements Serializable {
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    private static class Holder {
        static Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

 在单例对象内部创建一个静态内部类,因为内部类可以访问外部类的私有方法,所以在内部类里创建单例对象并赋值给静态变量,所以是线程安全的

JDK中单例模式的体现