多线程编程(十)——多线程与单例模式安全性详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/swadian2008/article/details/100178637

目录

一、饿汉模式和懒汉模式实现单例

1、饿汉模式——立即加载(线程安全)

2、懒汉模式——延迟加载(非线程安全)

3、DCL双检查锁机制确保延迟加载的线程安全

二、使用静态内部类和static代码块实现单例模式

1、静态内部类(线程安全)

2、使用static代码块(线程安全)

三、序列化的单例模式

1、单例模式被破坏

2、单例模式的实现

四、使用枚举类实现单例模式

1、反射破坏传统单例模式

2、枚举类的单例实现方式

(1)序列化安全

(2)反射安全

(3)枚举单例的实现


撰文目的:确保单例模式在多线程环境下是安全和正确的。

单例模式的三个特点:

1、构造方法私有

2、实例化的变量引用私有化

3、获取实例的方法公有

一、饿汉模式和懒汉模式实现单例

1、饿汉模式——立即加载(线程安全)

立即加载是在使用类的时候已经将对象创建完毕,立即加载有“着急”、“迫切”的含义,因此也叫饿汉模式。

测试代码:饿汉模式在调用方法前,对象已经被创建

public class MyService {

    // static 立即加载——饿汉模式
    private static MyService myService = new MyService();

    private MyService(){}

    public static MyService getInstance(){
        return myService;
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(MyService.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

2、懒汉模式——延迟加载(非线程安全)

延迟专加载实在调用get()方法时实例才被创建,延迟加载有“缓慢”,“不急迫”的含义,所以也称为懒汉模式。

测试代码:懒汉模式单例模式在多线程环境下存在严重的线程安全问题

public class MyService {

    // 延迟加载——懒汉模式
    private static MyService myService;

    private MyService(){}

    public static MyService getInstance(){
        try {
            if(myService == null){
                // 延迟加载
                Thread.sleep(3000);
                myService = new MyService();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myService;
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(MyService.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

因为对象在被调用时才会实例化对象,所以如果有多个线程同时调用实例化方法,根本不能保证实例化对象的唯一性。

3、DCL双检查锁机制确保延迟加载的线程安全

DCL是大多数多线程结合单例模式使用的解决方案。

第一次检查时,主要是避免在有实例的情况下,还去执行同步代码块,提高运行效率

第二次检查,是检测对象是否已经被创建,保证单例模式

测试代码:Double——Checked Lock 双检查锁机制,定义单例对象最好用volatile关键字来修饰,保证可见性!!!

public class MyService {

    // 延迟加载——懒汉模式
    private static MyService myService;

    private MyService(){}

    public static MyService getInstance(){
        try {
            if(myService == null){// 第一次检查,使不需要同步的代码异步执行
                Thread.sleep(3000);
                synchronized (MyService.class) {
                    if(myService == null){// 第二次检查,保证单例模式
                        // 延迟加载
                        myService = new MyService();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myService;
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(MyService.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

二、使用静态内部类和static代码块实现单例模式

两者在实现单例模式上都是利用static关键字的特性,即对象在使用前就已经被初始化,下面来分别看一下各自的实现形式。

1、静态内部类(线程安全)

使用静态内部类的好处——避免静态实例的加载,初始化时只加载Class文件,减小初始化的负载

public class MyService {

    private MyService(){}

    // 使用私有静态内部类
    private static class MyObjectHandler{
        private static MyService myService = new MyService();
    }

    public static MyService getInstance(){
        return MyObjectHandler.myService;
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(MyService.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

2、使用static代码块(线程安全)

静态代码块跟static关键字一样,使对象在使用的时候就已经被创建了,利用这一特性来实现单例模式,可以确保线程安全。

测试代码:

public class MyService {

    private MyService(){}

    private static MyService myService = null;

    static {// 使用静态代码块
        myService = new MyService();
    }

    public static MyService getInstance(){
        return myService;
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(MyService.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

三、序列化的单例模式

1、单例模式被破坏

单例模式在遇到序列化和反序列化时会遭到破坏,因为序列化前对象和反序列化后对象不是同一个对象

原因:反序列化时会通过反射调用无参的构造方法创建一个新的对象

代码测试:序列化前后对象不一样

public class MyService implements Serializable{

    private static final long serialVersionUID = 1L;

    private MyService(){}

    // 使用内部类方式
    private static class MyServiceHandler{
        private static MyService myService = new MyService();
    }

    public static MyService getInstance(){
        return MyServiceHandler.myService;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        /**使用输出流保存对象*****************************/
        MyService myService = MyService.getInstance();
        // "myService.txt" - 其实这个文件具体是什么都无所谓,主要是需要有个名字在这里
        FileOutputStream output = new FileOutputStream(new File("myService.txt"));
        ObjectOutputStream objectOut = new ObjectOutputStream(output);
        objectOut.writeObject(myService);
        objectOut.close();
        output.close();
        System.out.println(myService.hashCode());
        /**使用输入流读取对象******************************/
        FileInputStream fileInput = new FileInputStream(new File("myService.txt"));
        ObjectInputStream objectInput = new ObjectInputStream(fileInput);
        MyService service = (MyService) objectInput.readObject();
        objectInput.close();
        fileInput.close();
        System.out.println(service.hashCode());
    }
}

测试结果:

造成这种现象的具体原因,我们可以简单的看一下反序列化的源码和实现:

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        // 代码省略
        try {
            Object obj = readObject0(false);//找到这个方法
            // 代码省略
        } finally {
            // 代码省略
        }
    }

private Object readObject0(boolean unshared) throws IOException {
        // 代码省略
        try {
            switch (tc) {
                // 代码省略
                case TC_OBJECT:// 看到这里,如果读的是对象,将调用下边的方法
                    return checkResolve(readOrdinaryObject(unshared));
                // 代码省略     
            }
        } finally {
            // 代码省略
        }
    }

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        // 代码省略
        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();
        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }
        Object obj;
        try {
            // 重点是这句代码,通过反射生成了新对象!!!
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        // 代码省略
        return obj;
    }

是的,我们清楚的看到:obj = desc.isInstantiable() ? desc.newInstance() : null;它生成了一个新的对象!

2、单例模式的实现

实现序列化的单例模式,只要在需要实现单例模式的类中定义readResolve()方法,返回跟序列化前相等的对象就可以了。

用途:readResolve()方法用来替换从流中读取的对象,唯一的用途是强制执行单例。

测试代码:向类中添加了readResolve()方法

public class MyService implements Serializable{

    private static final long serialVersionUID = 1L;

    private MyService(){}

    // 使用内部类方式
    private static class MyServiceHandler{
        private static MyService myService = new MyService();
    }

    public static MyService getInstance(){
        return MyServiceHandler.myService;
    }

    // 为保证单例模式添加的方法
    protected Object readResolve(){
        return MyServiceHandler.myService;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        /**使用输出流保存对象*****************************/
        MyService myService = MyService.getInstance();
        // "myService.txt" - 其实这个文件具体是什么都无所谓,主要是需要有个名字在这里
        FileOutputStream output = new FileOutputStream(new File("myService.txt"));
        ObjectOutputStream objectOut = new ObjectOutputStream(output);
        objectOut.writeObject(myService);
        objectOut.close();
        output.close();
        System.out.println(myService.hashCode());
        /**使用输入流读取对象******************************/
        FileInputStream fileInput = new FileInputStream(new File("myService.txt"));
        ObjectInputStream objectInput = new ObjectInputStream(fileInput);
        MyService service = (MyService) objectInput.readObject();
        objectInput.close();
        fileInput.close();
        System.out.println(service.hashCode());
    }
}

测试结果:

为了使原因更清晰,我们可以回过头来看一下readOrdinaryObject()源码的具体实现:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        // 代码省略
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        // 如果readResolve()方法存在
        {
            // 通过readResolve()方法创建实例
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                // 在这里进行实例的替换,维持单例模式!!!
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

源码中,判断反序列化类中是否存在readResolve()方法,如果存在,就通过反射调用这个方法创建一个实例,在最后进行替换。

四、使用枚举类实现单例模式

1、反射破坏传统单例模式

传统的单例模式可以通过反射进行攻击!

借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

测试代码:反射对单例模式的破坏

public class MyService implements Serializable{

    private static final long serialVersionUID = 1L;

    private MyService(){}

    // 使用内部类方式
    private static class MyServiceHandler{
        private static MyService myService = new MyService();
    }

    public static MyService getInstance(){
        return MyServiceHandler.myService;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        MyService myServiceA = MyServiceHandler.myService;
        MyService myServiceB = MyServiceHandler.myService;
        // 获取构造器
        Constructor<MyService> constructor = MyService.class.getDeclaredConstructor();
        // 暴力破除私有化
        constructor.setAccessible(true);
        // 反射调用私有化构造方法实例化对象
        MyService myService = constructor.newInstance();
        System.out.println("myServiceA:"+myServiceA.hashCode());
        System.out.println("myServiceB:"+myServiceB.hashCode());
        System.out.println("反射创建myService:"+myService.hashCode());
    }
}

测试结果:

2、枚举类的单例实现方式

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

                                                                                                                                     ——《Effective java》

(1)序列化安全

枚举序列化是由JVM保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

在枚举类型的序列化和反序列化上,java做了特殊的规定:在序列化时,java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性。

(2)反射安全

反射不能创建枚举实例(源码分析待续...

(3)枚举单例的实现

把需要实例化的对象设计为枚举类,枚举类中的方法跟普通类中的方法定义是一样的。只不过,需要为这个枚举类定义一个枚举变量才可以调用到这些方法,而这个枚举变量总是唯一的(也就是单例)。

测试代码:枚举单例——实际上枚举就是用来替代常量的,而我们知道常量总是固定不变的,唯一的

public enum MyObject {

    MYOBJECT;

    public MyObject getInstance(){
        return MYOBJECT;
    }

    public void doSomthing(){
        System.out.println(Thread.currentThread().getName()+"——获取变量Hash值:"+MyObject.MYOBJECT.getInstance().hashCode());
    }

    static class DemoThread extends Thread{

        private MyObject myObject;

        public DemoThread(MyObject myObject){
            this.myObject = myObject;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"——获取变量Hash值:"+myObject.hashCode());
        }
    }

    public static void main(String[] args) {
        MyObject myObject = MyObject.MYOBJECT.getInstance();
        DemoThread a = new DemoThread(myObject);
        DemoThread b = new DemoThread(myObject);
        DemoThread c = new DemoThread(myObject);
        a.start();
        b.start();
        c.start();
        myObject.doSomthing();
    }
}

测试结果:枚举类是实现单例最简单,也是最安全的方式,它是最好的!!!

猜你喜欢

转载自blog.csdn.net/swadian2008/article/details/100178637