• Java单例模式之总有你想不到的知识


    文章目录

    Java单例模式

    单例模式是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建 对象的最佳方式

    单例模式确保在一个应用程序中某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 单例实例。

    满足条件

    单例模式只应在有真正的“单一实例”的需求时才可使用:

    1. 单例类只能有一个实例
    2. 单例类必须自己创建自己的唯一实例
    3. 单例类必须给所有其他对象提供这一实例

    两种形式

    Java中实现单例模式可以通过两种形式实现:

    • 懒汉模式 (类加载时不初始化)
    • 饿汉模式 (在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快)

    设计要求

    编写单例必须满足下面的条件:

    1. 构造方法变成私有
    2. 提供一个静态方法获取单实例对象

    饿汉模式

    饿汉模式基于classloader机制避免了多线程的同步问题(静态初始化将保证在任何线程能够访问到域之前初始化它),不过,instance在类装载时就实例化,这时候初始化instance显然没有达到懒加载(lazy loading)的效果

    饿汉单例相对比较容易理解,一般表现为以下两种形式:

    package com.shixun.design.singleton;
    
    public class  Singleton1  { 
        private static Singleton1 instance = new Singleton1();
    
        // 私有构造方法,保证外界无法直接实例化。 
        private  Singleton1()  { 
        }
        // 通过公有的静态方法获取对象实例 
        public  static  Singleton1  getInstance()  { 
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    也可以将静态对象初始化放在静态代码块中

    package com.shixun.design.singleton;
    
    public class  Singleton2  { 
        private static Singleton2 instance = null;
        // 对象初始化放在静态代码块中
        static { 
            instance = new Singleton2();
        }
        // 私有构造方法,保证外界无法直接实例化。
        private  Singleton2()  { 
        }
    
        // 通过公有的静态方法获取对象实例
        public  static  Singleton2  getInstance()  { 
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    懒汉方式

    实现单例模式能够提高类加载性能,但是和饿汉模式借助与JVM的类加载内部同步机制实现了线程安全不同,需要在延迟加载时注意单例实例的线程安全性,如果简单粗暴的实现,在多线程环境中将引起运行异常。

    例如下面代码将引起运行异常:

    package com.shixun.design.singleton;
    
    public class  Singleton3  { 
        private static Singleton3 instance;
    
        private  Singleton3()  { 
        }
    
        public  static  Singleton3  getInstance()  { 
            if (instance == null) { 
                instance = new Singleton3();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上述代码多线程同时访问时可能会产生多个示例,甚至会破坏实例,违背单例的设计原则

    用下面代码也能测试出来:

    package com.shixun.design.singleton;
    
    public class  SingletonTest  implements  Runnable{ 
        @Override
        public  void  run()  { 
            Singleton3 singleton3 =Singleton3.getInstance();
            System.out.println(singleton3);
        }
    
        public  static  void  main(String[] args)  { 
            for (int i=0;i<10;i++){ 
                SingletonTest myThread = new SingletonTest();
                Thread thread = new Thread(myThread, String.valueOf(i));
                Thread thread2 = new Thread(myThread, String.valueOf(i));
                Thread thread3 = new Thread(myThread, String.valueOf(i));
                thread.start();
                thread2.start();
                thread3.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    懒汉式多线程解决方案

    synchronized

    可以为返回单例实例的方法设置同步用来保证线程安全性

    package com.shixun.design.singleton;
    
    public class  Singleton4  { 
        private static Singleton4 instance;
    
        private  Singleton4()  { 
        }
    
        public  synchronized  static  Singleton4  getInstance()  { 
            if (instance == null) { 
                instance = new Singleton4();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这种写法能够在多线程中很好的工作,而且看起来它也具备很好的懒加载(lazy loading),但遗憾的是,由于整个方法被同步,因此效率相对较低

    双检查锁方式

    使用双检查锁需要进行两次instance == null的判断

    • 第一次判断没有锁,如果install不为null直接返回单实例对象,提高效率

    • 第二次判断防止多线程创建多个实例,假如A和B 两个线程同时争抢synchronized锁,A先争抢到锁,B 等待,A线程instance赋值实例化对象,释放锁,B线程获取到到锁,如果没有第二次判断的话,直接又会创建对象,那么就不符合单例要求

    并且还需要为这个静态对象加上volatile关键字, volatile在这里的作用是:通知其他线程及时更新变量;保证有序性,禁止指令重排序。

    通知其他线程及时更新变量还简单明了,第二个作用是这样的,我们举例说明一下:

    原因举例说明: 在执行instance = new Singleton()语句时,一共是有三步操作的。

    1. 堆中分配内存

    2. 调用构造方法进行初始化

    3. 将instance引用指向内存地址。

    在这三步有可能会产生指令重排序即有两种结果可能产生: 123与132(不管怎么重排序,单线程程序 的执行结果不会改变)

    如果A线程执行到 instance = new Singleton() ,此时2 ,3发生重排序,选执行3,则instance已经不为 null,但是指向的对象还未初始化完成,如果此时B对象判断instance 不为null就会直接返回一个未初始 化完成的对象。

    双检查锁方式代码如下:

    package com.shixun.design.singleton;
    
    public class  Singleton5  { 
        private volatile static Singleton5 instance;
    
        private  Singleton5()  { 
        }
    
        // 使用双检查锁方式
        public  static  Singleton5  getInstance()  { 
            if (instance == null) { 
                synchronized(Singleton5.class){ 
                    if(instance == null){ 
                        instance = new Singleton5();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    再测一下,没有问题:

    package com.shixun.design.singleton;
    
    public class  SingletonTest  implements  Runnable{ 
        @Override
        public void run() { 
            Singleton5 singleton5 =Singleton5.getInstance();
            System.out.println(singleton5);
        }
    
        public static void main(String[] args) { 
            for (int i=0;i<10;i++){ 
                SingletonTest myThread = new SingletonTest();
                Thread thread = new Thread(myThread, String.valueOf(i));
                Thread thread2 = new Thread(myThread, String.valueOf(i));
                Thread thread3 = new Thread(myThread, String.valueOf(i));
                Thread thread4 = new Thread(myThread, String.valueOf(i));
                Thread thread5 = new Thread(myThread, String.valueOf(i));
                thread.start();
                thread2.start();
                thread3.start();
                thread4.start();
                thread5.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    静态内部类

    之前提到了,静态初始化将在实例被任何线程访问到之前对其进行初始化,因此,可以借助于这个特性对懒汉单例进行改造:

    静态内部类加载机制:使用时候才被加载,而且多线程情况下, classloader能够保证只加载一份字节码

    代码如下:

    package com.shixun.design.singleton;
    
    public class  Singleton6  { 
    
        private static class  SingletonHolder  { 
            private final static Singleton6 Instance = new Singleton6();
        }
    
        private  Singleton6()  { 
        }
    
        public  static  final  Singleton6  getInstance()  { 
            return SingletonHolder.Instance;
        }
    
        public  void  say()  { 
            System.out.println("调用了say方法");
        }
    
        public  static  void  sayHello()  { 
            System.out.println("调用了sayHello方法");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    测试也OK:

    package com.shixun.design.singleton;
    
    public class  SingletonTest  implements  Runnable{ 
        @Override
        public void run() { 
            Singleton6 singleton6 =Singleton6.getInstance();
            System.out.println(singleton6);
            singleton6.say();
        }
    
        public static void main(String[] args) { 
            for (int i=0;i<10;i++){ 
                SingletonTest myThread = new SingletonTest();
                Thread thread = new Thread(myThread, String.valueOf(i));
                Thread thread2 = new Thread(myThread, String.valueOf(i));
                Thread thread3 = new Thread(myThread, String.valueOf(i));
                Thread thread4 = new Thread(myThread, String.valueOf(i));
                Thread thread5 = new Thread(myThread, String.valueOf(i));
                thread.start();
                thread2.start();
                thread3.start();
                thread4.start();
                thread5.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    枚举(别瞎用)

    JDK1.5之后引入了枚举,由于枚举的特性,可以利用其来实现单例,它不仅能避免多线程同步问 题,而且还能防止反序列化重新创建新的对象(序列化和反序列化后是同一个对象)

    代码如下

    package com.shixun.design.singleton;
    
    public enum  Singleton7 { 
        INSTANCE;
    
        public  void  say(){ 
            System.out.println("say ni hello!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    测试一下:

    package com.shixun.design.singleton;
    
    public class  SingletonTest  implements  Runnable{ 
        @Override
        public void run() { 
            Singleton7 singleton7 = Singleton7.INSTANCE;
            System.out.println(singleton7.hashCode());
            singleton7.say();
        }
    
        public static void main(String[] args) { 
            for (int i=0;i<10;i++){ 
                SingletonTest myThread = new SingletonTest();
                Thread thread = new Thread(myThread, String.valueOf(i));
                Thread thread2 = new Thread(myThread, String.valueOf(i));
                Thread thread3 = new Thread(myThread, String.valueOf(i));
                Thread thread4 = new Thread(myThread, String.valueOf(i));
                Thread thread5 = new Thread(myThread, String.valueOf(i));
                thread.start();
                thread2.start();
                thread3.start();
                thread4.start();
                thread5.start();
            }
        }
    
        /**
     * 生写懒汉式多线程问题
     */
        private static void method01() { 
            for (int i=0;i<100;i++){ 
                SingletonTest myThread = new SingletonTest();
                Thread thread = new Thread(myThread, String.valueOf(i));
                Thread thread2 = new Thread(myThread, String.valueOf(i));
                Thread thread3 = new Thread(myThread, String.valueOf(i));
                thread.start();
                thread2.start();
                thread3.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

  • 相关阅读:
    【数据结构与算法】<==>二叉树下
    C语言项目Tinyhttp的学习笔记
    内省机制(操作javaBean的信息)
    IDEA2023新UI回退老UI
    软件工程导论概述----软件的生命周期
    Apache Hudi vs Delta Lake:透明TPC-DS Lakehouse性能基准
    D52【python 接口自动化学习】- python基础之模块与标准库
    AI角色对环境信息的感知方式
    基于springboot在线考试报名系统毕业设计源码031706
    LeetCode 面试题 05.06. 整数转换
  • 原文地址:https://blog.csdn.net/band_mmbx/article/details/126314860