单例的8种实现

单例模式是比较简单的设计模式,但是涉及到的知识点还是挺多,比如并发模式下的单例、序列化反序列化情况下保证单例、反射情况下保证单例,下面来看看各种情况下怎么保证单例。

    单例模式最基本的组成构造函数私有化、即不能随便创建对象;对外提供一个静态方法来获取对象。

 一、饿汉模式

1、饿汉模式实现

public class Single {
    /**
     * 类成员变量
     */
    private static Single single = new Single();
    private Single() {
        System.out.println("私有构造函数");
    }
    public static Single getInstance() {
        return single;
    }
}
 

2、饿汉模式单例对象创建流程 

(1)、invokestatic 指令触发类加载、初始化。

(2)、类加载流程:类加载(Class对象生成)、连接(验证.class 文件、准备、解析)、初始化。

(3)、类成员变量在准备阶段被分配内存空间并赋予初始值,分配的空间是在方法区。

(4)、初始化阶段是执行类构造器<clinit> 方法的过程。

(5)、<clinit>类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。编译器收集的顺序是由语句在原文中出现的顺序决定的。

(6)、类成员变量初始化:定义变量直接赋值、静态代码块赋值,不管是那种赋值方式都是使用 <cinit>类构造器中putstatic 指令来给类成员变量赋值。

(7)、虚拟机会保证一个类的<cinit> 方法在多线程环境中被正确的加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<cinit>方法,其他线程都要阻塞等待,直到活动线程执行<clinit>方法完毕。

(8)、一个线程执行<clinit> 类构造方法,其他线程虽然会被阻塞,但如果执行<clinit> 方法的那条线程退出<clinit> 方法后,其他线程唤醒之后不会在进入<clinit> 方法。同一个类加载器下,一个类型只会初始化一次。

(9)、类变量初始化结束

3、线程安全问题

    饿汉模式在<clinit> 类构造器中主动创建对象,按照上面的创建流程发现、饿汉单例模式是线程安全的。  

二、懒汉模式

1、懒汉模式实现

public class Single {
    /**
     * 类成员变量
     */
    private static Single single ;
    private Single() {
        System.out.println("私有构造函数");
    }
    public static Single getInstance() {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}
 

2、单例对象创建时间点

    他这个懒汉模式懒在哪里,当调用invokestatic 指令之后进行加载类、连接、初始化;对象的创建不是在类初始化<clinit>方法中而是延迟到了静态方法中……

3、懒汉模式线程安全问题

if(null == single){
    Single  single= new Single() 
}
 

4、多线程环境下执行存在线程安全问题

ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 30; i++) {
    service.execute(new Runnable() {
        @Override
        public void run() {
            Single single = getInstance();
            System.out.println(single);
        }
    });
}
 

5、执行结果

私有构造函数私有构造函数com.sb.design.single.Single@2deeefc5私有构造函数com.sb.design.single.Single@35655ed5com.sb.design.single.Single@2e1b63f0com.sb.design.single.Single@35655ed5com.sb.design.single.Single@2e1b63f0

    多次调用构造函数创建对象而且对象地址也不相同……

    唯一性判断在单线程环境没问题,但是在多线程并发执行情况下这个判断条件不能保证原子性,也不能保证复合原子性,所以这样的写法在多线程环境下无法保证单例。

三、静态内部类(懒汉+饿汉)

1、静态内部类实现单例模式

public class Single implements Serializable {
    static {
        System.out.println("1、外部类加载、链接、初始化");
    }
    private Single() {
        System.out.println("4、调用私有构造函数初始化单例对象");
    }
    /**
     * 获取枚举类对象
     *
     * @return
     */
    private static class InnerSingle {
        static {
            System.out.println("3、静态内部类加载、链接、初始化");
       }
        private static final Single SINGLE = new Single();
    }

    /**
     * 对外发布单例对象
     *
     * @return
     */
    public static Single getInstance() {
        System.out.println("2、外部类加载、链接、初始化完成");
        return InnerSingle.SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Single single = Single.getInstance();
        Single single1 = Single.getInstance();
        System.out.println(single == single1);
    }
}
 

2、静态内部类执行流程

1、外部类加载、链接、初始化2、外部类加载、链接、初始化完成3、静态内部类加载、链接、初始化4、调用私有构造函数初始化单例对象2、外部类加载、链接、初始化完成true

3、为何说内部类单例是懒汉+饿汉模式我们分析一下静态内部类的加载顺序

(1)、当有getstatic、setstatic、invokestatic 等指令调用外部类则会触发外部类的加载、链接、初始化过程,但是在这过程不会触发静态内部类的加载(静态内部类、非静态内部类的加载和外部类的加载无关),这里我们调用了外部类静态方法即触发了invokestatic 指令,所以进行了外部类的加载、链接、初始化流程的执行。(2)、当外部类初始化结束后开始执行外部类静态方法(3)、外部类静态方法中调用了内部类的静态常量,这样内部类又触发了getstatic 指令(4)、内部类触发getstatic指令开始执行内部类的类加载、连接、初始化流程(5)、内部类初始化就是内部类的类构造器执行过程即<clinit> 方法执行(6)、按照开始分析的饿汉模式加载流程知道clinit方法中有putstatic 指令即给内部类的静态变量赋值(7)、putstatic 指令赋值的具体对象就是调用外部类的私有构造函数创建的单例对象(8)、结束了静态内部类单例的生成流程,按照<clinit> 方法执行是线程安全的来看,静态内部类也是线程安全的。

4、多线程测试

public static void main(String[] args) throws IOException, ClassNotFoundException {
    ExecutorService executor = Executors.newFixedThreadPool(50);
    for (int i = 0; i < 100; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Single single = Single.getInstance();
                Single single1 = Single.getInstance();
                System.out.println(single == single1);
            }
        });
    }
    executor.shutdown();
}
 

5、多线程测试执行结果

1、外部类加载、链接、初始化2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成3、静态内部类加载、链接、初始化4、调用私有构造函数初始化单例对象2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成true2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成true2、外部类加载、链接、初始化完成2、外部类加载、链接、初始化完成true……

    只要看外部类私有构造函数调用次数就能证明线程安全,其实从上面的输出也证明了<clinit> 类构造器方法的执行是线程安全的。

四、懒汉模式-多线程环境加类锁

1、懒汉模式线程安全实现

public static Single getInstance() {
    synchronized (Single.class) {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}
    使用synchronized锁类,这样写感觉没问题,但是所有线程都需要获得类锁才能判断是不是对象已创建,这样这个锁竞争很激烈,性能也就更低。

五、懒汉模式-多线程环境双重锁判断

1、代码实现

public static Single getInstance() {
    if (null == single) {
        synchronized (Single.class) {
            if (null == single) {
                single = new Single();
            }
        }
    }
    return single;
}
    双重锁校验相比直接添加类锁,锁竞争没那么激烈,有性能提升但是,这种写法又带来了一个严重的问题即指令重排。

更严格的说这种写法不是线程安全的,分析原因如下:

2、对象创建流程

1、通过new 指令判断所指向的类是否已加载,如果未加载则进行加载。2、给对象在堆内存分配空间即指定一块对内存给新建对象。3、对象初始化即<init> 对象构造函数执行。4、将新建对象地址赋值给引用变量。    但是在编译和运行时会对现有代码进行优化即指令重排序,排序之后对象创建顺序有可能是1-2-3-4 也有可能是1-2-4-3 当出现 1-2-4-3 这种顺序的时候上上面的双重锁校验就有问题!

3、指令重排后的对象创建流程

    如果指令重排后是1-2-4-3 这样的顺序,当线程A执行到4即将对象内存地址赋值给引用变量,cpu资源被B线程占用,这时线程B判断不为空则直接返回single对象,但是这个对象没有被初始化;所以这样的写法在1-2-4-3 这样的执行顺序下是有问题的,而引起这个问题的根本原因是指令重排。

六、懒汉模式-指令重排

volatile 关键字是轻量级锁,他能保证变量的可见性和防止指令重排(内存屏障)​​​​​​​

private static volatile Single single;
synchronized (Single.class) {
    if (null == single) {
        single = new Single();
    }
}
一个volatile关键字搞定;这样多线程环境下的单例模式就没问题了,可以安全使用!

七、使用序列化、反序列化破坏单例模式

1、序列化、反序列化破坏单例​​​​​​​

public class Single implements Serializable {
    /**
     * 类成员变量
     */
    private static volatile Single single;
    private Single() {
        System.out.println("私有构造函数");
    }
    /**
     * 双重锁校验
     *
     * @return
     */
    public static Single getInstance() {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException, NoSuchMethodException, IOException, ClassNotFoundException {
        Single single = getInstance();
        // 序列化
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "singles.text");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(single);
        // 反序列化
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Single single1 = (Single) objectInputStream.readObject();
        System.out.println(single == single1);
    }
}
    其实看序列化、发序列化的过程你会发现也是调用的反射来创建对象;使用序列化破坏单利很简单,那怎么防止序列化、反序列化破坏单利模式?

2、一步步分析反序列化过程、了解是怎么破坏的单例

(1)、从输入流获取对象

(2)、readObject0获取对象

(3)、获取类序列化描述符类,并使用反射获取反序列化新对象

(4)、使用constructor 构造函数对象创建新对象

(5)、readResolve方法判断,如果序列化类没有readResolve 方法则直接返回构造函数创建的对象,如果存在则获取readResolve 方法返回值

(6)、反射调用readResolve 方法

(7)、用readResolve 方法返回值替换使用constructor 构造函数器对象创建的新对象

    从上面流程看到,如果序列化类无readResolve 方法则直接返回通过constructor 构造函数类对象创建的新对象,如果readResolve 方法存在则返回该方法返回值,所以在单例类序列化时添加该方法,可以避免序列化、反序列化破坏单例!

八、懒汉单例模式-反射破坏单利

 因为返利能获取类中的任何变量并且能重新设置值,所以不管是添加一个计数器还是判断变量是否为空都是无法避免反射破坏单例的;下面来看看使用计数器和判断是否为null来阻止破坏单例模式是否有效。​​​​​​​

public class Single implements Serializable {
    private static volatile boolean flag = true;
    private static volatile Single single;
    /**
     * 类成员变量
     */
    private Single() throws BindException {
        if (!flag) {
            throw new BindException("多次创建对象");
        }
        flag = false;
    }

    /**
     * @return
     * @throws BindException
     */

    public static Single getSingle() throws BindException {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }

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

        Single single = Single.getSingle();
        Single single1 = Single.getSingle();
        System.out.println(single == single1);
        //反射设置属性
        Class<Single> singleClass = Single.class;
        Field field = singleClass.getDeclaredField("flag");
        field.setAccessible(true);
        //设置属性,满足创建对象条件
        field.set(single, true);
        Constructor<Single> constructor = singleClass.getDeclaredConstructor(null);
        Single single2 = constructor.newInstance(null);
        System.out.println(single == single2);
    }
}
 

九、枚举类解决序列化、反序列化以及反射等的相关问题

1、代码实现​​​​​​​

public class Single implements Serializable {
    /**
     * 类成员变量
     */
    private Single() {
        System.out.println("私有构造函数");
    }
    /**
     * 内部类
     */
    private enum InnerClass implements Serializable {
        MY_SINGLE;
        private final Single single;
        /**
         * 枚举类无惨构造函数
         */
        InnerClass() {
            System.out.println("内部类构造函数");
            single = new Single();
        }
        /**
         * 私有方法
         *
         * @return
         */

        private Single getSingle() {
            return single;
        }
    }

    /**
     * 获取枚举类对象
     *
     * @return
     */

    public static InnerClass getEnum() {
        return InnerClass.MY_SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InnerClass innerClass = getEnum();
        Single single = innerClass.getSingle();
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "single.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(innerClass);
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        InnerClass innerClass1 = (InnerClass) objectInputStream.readObject();
        System.out.println(innerClass == innerClass1);
        Single single2 = innerClass1.getSingle();
        System.out.println(single == single2);
    }
}
 

2、执行结果​​​​​​​

内部类构造函数私有构造函数truetrue
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值