• Java多线程编程(六)单例模式与多线程


      在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。

      一、立即加载/“饿汉模式”

      立即加载就是使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。立即加载/“饿汉模式”是在调用方法前,实例已经被创建了。

      示例:从输出结果可以看出,虽然3个线程同时执行了同一个对象的同一个方法,但是输出的hashCode值是一样的,说明对象是同一个,因为private static MyObject myObject = new MyObject();所以也就实现了立即加载型单例设计模式。

    package test;
    
    public class MyObject {
    
        //立即加载 == 懒汉模式
        private static MyObject myObject = new MyObject();
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            //缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。
            return myObject;
        }
    
    }
    package extthread;
    
    import test.MyObject;
    
    public class MyThread extends Thread {
    
        @Override
        public void run() {
            System.out.println(MyObject.getInstance().hashCode());
        }
    
    }
    package test.run;
    
    import extthread.MyThread;
    
    public class Run {
    
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    
    }
    180612070
    180612070
    180612070
    

      二、延迟加载/“懒汉模式”

      延迟加载就是在调用get()方法时实例才被创建,常见的办法就是在get()方法中进行new实例化。

      (1)延迟加载/“懒汉模式”实现

      示例:由于只有一个线程,所以从结果来看确实取得了一个对象的实例,但是在多线程环境中,就会取出多个实例的情况,这与单例模式的初衷是相违背的。

    package test;
    
    public class MyObject {
    
        private static MyObject myObject;
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            //延迟加载
            if (myObject != null) {
            } else {
                myObject = new MyObject();
            }
            return myObject;
        }
    
    }
    package extthread;
    
    import test.MyObject;
    
    public class MyThread extends Thread {
    
        @Override
        public void run() {
            System.out.println(MyObject.getInstance().hashCode());
        }
    
    }
    package test.run;
    
    import extthread.MyThread;
    
    public class Run {
    
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            t1.start();
        }
    
    }
    51011016
    

       (2)延迟加载/“懒汉模式”缺点

      示例:验证多线程中延迟加载的错误性。从输出可以看出来,3个线程创建了3个不同的实例,这是因为3个线程没有实现同步,即一开始都判断没有实例,所以都创建了实例。

    package test;
    
    public class MyObject {
    
        private static MyObject myObject;
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                if (myObject != null) {
                } else {
                    //模拟在创建对象前做一些准备工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return myObject;
        }
    
    }
    package extthread;
    
    import test.MyObject;
    
    public class MyThread extends Thread {
    
        @Override
        public void run() {
            System.out.println(MyObject.getInstance().hashCode());
        }
    
    }
    package test.run;
    
    import extthread.MyThread;
    
    public class Run {
    
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    
    }
    379353320
    1026810021
    1839450825
    

       (3)延迟加载/“懒汉模式”解决方案

      ①声明synchronized关键字

      示例:修改getInstance()方法,加上synchronized关键字,既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可,这样就可以得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,就必须等待上一个线程释放锁之后,才可以继续执行。执行完需要花费9秒的时间。

    package test;
    
    public class MyObject {
    
        private static MyObject myObject;
    
        private MyObject() {
        }
    
        synchronized public static MyObject getInstance() {
            try {
                if (myObject != null) {
                } else {
                    //模拟在创建对象前做一些准备工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return myObject;
        }
    
    }
    379353320
    379353320
    379353320
    

       ②尝试同步代码块

      示例:之前的加synchronized关键字是对整个方法上锁,这里虽然使用了同步代码块,但是几乎是整个方法里面的代码,虽然可以达到效果,但这对运行效率来说也是不利的。执行总时间也是9秒。

    package test;
    
    public class MyObject {
    
        private static MyObject myObject;
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                // 这种方法等同于
                // synchronized public static MyObject getInstance()
                // 效率一样很低,全部代码被上锁。
                synchronized (MyObject.class) {
                    if (myObject != null) {
                    } else {
                        Thread.sleep(3000);
    
                        myObject = new MyObject();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return myObject;
        }
    
    }
    621856954
    621856954
    621856954
    

       ③针对某些重要的代码进行单独的同步

      示例:同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升。虽然不对sleep(3000)上锁这个思路很对,但是还是无法解决问题啊,3个线程还是分别都创建了一个实例。

    package test;
    
    public class MyObject {
    
        private static MyObject myObject;
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                if (myObject != null) {
                } else {
                    // 模拟在创建对象之前做一些准备性的工作
                    Thread.sleep(3000);
                    //使用synchronized (MyObject.class)
                    //虽然部分代码被上锁
                    //但是还是有非线程安全问题
                    synchronized (MyObject.class) {
                        myObject = new MyObject();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return myObject;
        }
    
    }
    180612070
    621856954
    686587801
    

       ④使用DCL双检查锁机制

      示例:采用双重检查锁机制,成功解决了延迟加载遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。整个代码执行时间远少于9秒。

    package test;
    
    public class MyObject {
    
        private volatile static MyObject myObject;
    
        private MyObject() {
        }
    
        //使用双监测机制来解决问题,既保证了不需要同步代码的异步执行性,有保证了单例的效果
        public static MyObject getInstance() {
            try {
                if (myObject != null) {
                } else {
                    //模拟在创建对象之前做的一些准备性的工作
                    Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        if (myObject == null) {
                            myObject = new MyObject();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return myObject;
        }
        //称为Double-Check Locking
    }
    686587801
    686587801
    686587801
    

      三、使用静态内置类实现单例模式

      除了DCL外,其他的方法也能达到同样的效果。

      示例:通过使用静态内之类的方式也可以实现3个线程只创建一个对象的需求。

    package test;
    
    public class MyObject {
    
        //内部静态类方式
        private static class MyObjectHandler {
            private static MyObject myObject = new MyObject();
        }
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return MyObjectHandler.myObject;
        }
    
    }
    180612070
    180612070
    180612070
    

      四、序列化与反序列化的单例模式实现

      静态内置类可以达到线程安全问题,但如果需要序列化对象时,使用默认的方式运行得到的结果还是多例的。

      示例1:不使用readResolve()方法时,得到的不是同一个对象。

    package test;
    
    import java.io.ObjectStreamException;
    import java.io.Serializable;
    
    public class MyObject implements Serializable {
    
        private static final long serialVersionUID = 888L;
    
        //静态内置类方式
        private static class MyObjectHandler {
            private static final MyObject myObject = new MyObject();
        }
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return MyObjectHandler.myObject;
        }
    
    //    protected Object readResolve() throws ObjectStreamException {
    //        System.out.println("调用了readResolve方法");
    //        return MyObjectHandler.myObject;
    //    }
    
    }
    package test.run;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    import test.MyObject;
    
    public class SaveAndRead {
    
        public static void main(String[] args) {
            try {
                MyObject myObject = MyObject.getInstance();
                FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));
                ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
                oosRef.writeObject(myObject);
                oosRef.close();
                fosRef.close();
                System.out.println(myObject.hashCode());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            try {
                FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));
                ObjectInputStream iosRef = new ObjectInputStream(fisRef);
                MyObject myObject = (MyObject) iosRef.readObject();
                iosRef.close();
                fisRef.close();
                System.out.println(myObject.hashCode());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    1975012498
    990368553
    

       示例2:在反序列化中使用readResolve()方法时,就会得到同一个对象。

    package test;
    
    import java.io.ObjectStreamException;
    import java.io.Serializable;
    
    public class MyObject implements Serializable {
    
        private static final long serialVersionUID = 888L;
    
        //静态内置类方式
        private static class MyObjectHandler {
            private static final MyObject myObject = new MyObject();
        }
    
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return MyObjectHandler.myObject;
        }
    
        protected Object readResolve() throws ObjectStreamException {
            System.out.println("调用了readResolve方法");
            return MyObjectHandler.myObject;
        }
    
    }
    1975012498
    调用了readResolve方法
    1975012498
    

      五、使用static代码块实现单例模式

      静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式。

      示例:从输出结果也可以看出,使用静态代码块也可以解决问题。

    package test;
    
    
    public class MyObject {
    
        private static MyObject instance = null;
    
        private MyObject() {
        }
    
        static {
            instance = new MyObject();
        }
    
        public static MyObject getInstance() {
            return instance;
        }
    
    }
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    686587801
    

      六、使用enum枚举数据类型实现单例模式

      枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,也可以利用这个特性实现单例设计模式。

      示例:利用public enum MyObject{}枚举类,可以实现单例设计模式。

    package test;
    
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public enum MyObject {
        connectionFactory;
    
        private Connection connection;
    
        private MyObject() {
            try {
                System.out.println("调用了MyObject的构造器");
                String url = "jdbc:sqlserver://localhost:1079;databaseName=ghydb";
                String username = "sa";
                String password = "";
                String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
                Class.forName(driverName);
                connection = DriverManager.getConnection(url, username, password);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
        public Connection getConnection() {
            return connection;
        }
    }
    package extthread;
    
    import test.MyObject;
    
    public class MyThread extends Thread {
    
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(MyObject.connectionFactory.getConnection()
                        .hashCode());
            }
        }
    }
    调用了MyObject的构造器
    686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801

      七、完善使用enum枚举实现单例模式

      示例:六中的示例将枚举类进行曝露,违反了“职责单一原则”,修改MyObject类进行完善。

    package test;
    
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public class MyObject {
    
        public enum MyEnumSingleton {
            connectionFactory;
    
            private Connection connection;
    
            private MyEnumSingleton() {
                try {
                    System.out.println("创建MyObject对象");
                    String url = "jdbc:sqlserver://localhost:1079;databaseName=y2";
                    String username = "sa";
                    String password = "";
                    String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
                    Class.forName(driverName);
                    connection = DriverManager.getConnection(url, username,
                            password);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    
            public Connection getConnection() {
                return connection;
            }
        }
    
        public static Connection getConnection() {
            return MyEnumSingleton.connectionFactory.getConnection();
        }
    
    }

      

  • 相关阅读:
    为什么页面设计宽度要控制在960px
    RRDtool运用
    cacti监控jvm
    cacti安装
    rConfig v3.9.2 授权认证与未授权RCE (CVE-2019-16663) 、(CVE-2019-16662)
    Linux安全学习
    Github-Dorks与辅助工具
    警方破获超大DDoS黑产案,20万个僵尸网络运营商被抓
    SRC漏洞挖掘
    威胁情报木马病毒样本搜集
  • 原文地址:https://www.cnblogs.com/BigJunOba/p/8991353.html
Copyright © 2020-2023  润新知