《Java 多线程编程核心技术》笔记——第6章 单例模式与多线程

本文深入探讨了单例模式在多线程环境中的实现,对比了立即加载(饿汉模式)和延迟加载(懒汉模式),并详细分析了延迟加载的线程安全问题及解决方案,包括使用`synchronized`关键字、同步代码块、双检查锁(DCL)机制。此外,还介绍了静态内部类、序列化和反序列化、静态初始化块以及枚举类型的单例实现方式,强调了在多线程场景下确保单例正确性的关键点。
摘要由CSDN通过智能技术生成

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

本章的知识点非常重要,通过单例模式与多线程技术相结合,在这个过程中能发现很多以前从未考虑过的情况,一些不良的程序设计方法如果应用在商业项目中,将会遇到非常大的麻烦。本章的案例也将充分说明,线程与某些技术相结合时要考虑的事情有很多。在学习本章时只需要考虑一件事情,那就是:如何使单例模式遇到多线程是安全的、正确的

在标准的 23 个设计模式中,单例设计模式在应用中是比较常见的。但在常规的该模式教学资料介绍中,多数并没有结合多线程技术作为参考,这就造成在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。本章将介绍单例模式结合多线程技术在使用时的相关知识。

6.1 立即加载 / “饿汉模式”

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化。而立即加载从中文的语境来看,有 “着急”、“急迫” 的含义,所以也称为 “饿汉模式”。

立即加载 / “饿汉模式” 是在调用方法前,实例已经被创建了,来看一下实现代码。

  1. 创建公共类

    public class MyObject {
        private static MyObject object = new MyObject();
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    228428496
    228428496
    228428496
    

控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

6.2 延迟加载 / “懒汉模式”

什么是延迟加载?延迟加载就是在调用 get() 方法时实例才被创建,常见的实现办法就是在 get() 方法中进行 new 实例化。而延迟加载从中文语境来看,是 “缓慢”、“不急迫” 的含义,所以也称为 “懒汉模式”。

6.2.1 延迟加载 / “懒汉模式” 解析

延迟加载 / “懒汉模式” 是在调用方法时实例才被创建。一起来看一下实现代码。

  1. 创建公共类

    public class MyObject {
        private static MyObject object;
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            if (object == null) {
                object = new MyObject();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

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

    运行结果:

    1839047274
    

此实验虽然取得一个对象的实例,但如果是在多线程的环境中,就会出现取出多个实例的情况,与单例模式的初衷是相背离的。

6.2.2 延迟加载 / “懒汉模式” 的缺点

前面两个实验虽然使用 “立即加载” 和 “延迟加载” 实现了单例设计模式,但在多线程的环境中,前面 “延迟加载” 示例中的代码完全就是错误的,根本不能实现保持单例的状态。来看一下如何在多线程环境中结合 “错误的单例模式” 创建出 “多例”。

  1. 创建公共类

    public class MyObject {
        private static MyObject object;
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                if (object == null) {
                    Thread.sleep(3000);
                    object = new MyObject();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    1415852528
    4641705
    736192518
    

控制台打印出了 3 种 hashCode,说明创建出了 3 个对象,并不是单例的,这就是 “错误的单例模式”。如何解决呢?先看一下解决方案。

6.2.3 延迟加载 / “懒汉模式” 的解决方案

6.2.3.1 声明 synchronized 关键字

既然多个线程可以同时进入 getInstance() 方法,那么只需要对 getInstance() 方法声明 synchronized 关键字即可。

  1. 创建公共类

    public class MyObject {
        private static MyObject object;
        
        private MyObject() {
        }
    
        synchronized public static MyObject getInstance() {
            try {
                if (object == null) {
                    Thread.sleep(3000);
                    object = new MyObject();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    661713944
    661713944
    661713944
    

此方法加入同步 synchronized 关键字得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行。

6.2.3.2 尝试同步代码块

同步方法是对方法的整体进行持锁,这对运行效率来讲是不利的。改成同步代码块能解决吗?

  1. 创建公共类

    public class MyObject {
        private static MyObject object;
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                synchronized (MyObject.class) {
                    if (object == null) {
                        Thread.sleep(3000);
                        object = new MyObject();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    1833680959
    1833680959
    1833680959
    

此方法加入同步 synchronized 语句块得到相同实例的对象,但此种方法的运行效率也是非常低的,和 synchronized 同步方法一样是同步运行的。继续更改代码尝试解决这个缺点。

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

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升。

  1. 创建公共类

    public class MyObject {
        private static MyObject object;
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                if (object == null) {
    				Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        object = new MyObject();
                    }
                }  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    1211537232
    661713944
    676905244
    

此方法使同步 synchronized 语句块,只对实例化对象的关键代码进行同步,从语句的结构上来讲,运行的效率的确得到了提升。但如果是遇到多线程的情况下还是无法解决得到同一个实例对象的结果。到底如何解决 “懒汉模式” 遇到多线程的情况呢

6.3.3.4 使用 DCL 双检查锁机制

在最后的步骤中,使用的是 DCL 双检查锁机制来实现多线程环境中的延迟加载单例设计模式

  1. 创建公共类

    public class MyObject {
        private volatile static MyObject object;
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            try {
                if (object == null) {
                    Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        if (object == null) {
                            object = new MyObject();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    661713944
    661713944
    661713944
    

使用双重检查锁功能,成功地解决了 “懒汉模式” 遇到多线程的问题。DCL 也是大多数多线程结合单例模式使用的解决方案。

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

  1. 创建公共类

    public class MyObject {
        private static class MyObjectHandler {
            private static MyObject object = new MyObject();
        }
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return MyObjectHandler.object;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    228428496
    228428496
    228428496
    

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

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

  1. 创建公共类

    public class MyObject implements Serializable {
        private static class MyObjectHandler {
            private static MyObject object = new MyObject();
        }
        
        private MyObject() {
        }
    
        public static MyObject getInstance() {
            return MyObjectHandler.object;
        }
    
    //     在反序列化中使用readResolve方法 解决序列化问题。
    //     protected Object readResolve() {
    //         System.out.println("call readResolve method");
    //         return MyObjectHandler.object;
    //     }
    }
    
  2. 测试类

    public class SaveAndRead {
        public static void main(String[] args) {
            try {
                MyObject object1 = MyObject.getInstance();
                FileOutputStream out = new FileOutputStream(new File("myObjectFile.txt"));
                ObjectOutputStream outObject = new ObjectOutputStream(out);
                outObject.writeObject(object1);
                outObject.close();
                out.close();
                System.out.println(object1.hashCode());
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                FileInputStream in = new FileInputStream(new File("myObjectFile.txt"));
                ObjectInputStream inObject = new ObjectInputStream(in);
                MyObject object2 = (MyObject) inObject.readObject();
                inObject.close();
                in.close();
                System.out.println(object2.hashCode());
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    

    运行结果:

    325040804
    1452126962
    
  3. 解决办法就是在反序列化中使用 readResolve() 方法

    去掉如下代码的注释:

    // 在反序列化中使用readResolve方法 解决序列化问题。
    protected Object readResolve() {
    	System.out.println("call readResolve method");
    	return MyObjectHandler.object;
    }
    

    运行结果:

    325040804
    call readResolve method
    325040804
    

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

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

  1. 创建公共类

    public class MyObject {
    
        private static MyObject instance = null;
    
        private MyObject() {
        }
    
        static {
            instance = new MyObject();
        }
    
        public static MyObject getInstance() {
            return instance;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 5; i++) {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    661713944
    

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

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

  1. 创建公共类

    public enum MyObject {
        connectionFactory;
        private Connection connection;
    
        private MyObject() {
            try {
                System.out.println("call MyObject Constructor");
                String url = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8";
                String username = "root";
                String password = "123456";
                String driver = "com.mysql.cj.jdbc.Driver";
                Class.forName(driver);
                //do database connect
                connection = DriverManager.getConnection(url, username, password);
            } catch (SQLException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        
        public Connection getConnection() {
            return connection;
        }
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 5; i++) {
            	System.out.println(MyObject.connectionFactory.getConnection().hashCode());
            }
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    call MyObject Constructor
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    136389612
    

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

前面一节将枚举类进行暴露,违反了“职责单一原则”,下面进行完善

  1. 创建公共类

    public class MyObject {
        public enum MyEnumSingleton{
            connectionFactory;
            private Connection connection;
    
            private MyEnumSingleton() {
                try {
                    System.out.println("call MyObject2 Constructor");
                    String url = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8";
                    String username = "root";
                    String password = "123456";
                    String driver = "com.mysql.cj.jdbc.Driver";
                    Class.forName(driver);
                    //do database connect
                    connection = DriverManager.getConnection(url, username, password);
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
    
            public Connection getConnection() {
                return connection;
            }
        }
    
        public static Connection getConnection() {
            return MyEnumSingleton.connectionFactory.getConnection();
        }
    
    }
    
  2. 创建自定义线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 5; i++) {
                System.out.println(MyObject.getConnection().hashCode());
            }
        }
    }
    
  3. 测试类

    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();
        }
    }
    

    运行结果:

    call MyObject2 Constructor
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    8785325
    

6.8 本章总结

本章使用若干案例来阐述单例模式与多线程结合时遇到的情况与解决方法。本章也复习了不同单例模式的使用,使得以后再遇到单例模式时,就能从容面对多线程环境的情况了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值