zoukankan      html  css  js  c++  java
  • 安全发布对象—单例模式

    发布对象

    • 发布对象:使一个对象能够被当前范围之外的代码所使用
    • 对象溢出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见

    不正确的发布可变对象导致的两种错误:
    1.发布线程意外的所有线程都可以看到被发布对象的过期的值
    2.线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的

    Java 线程安全性中的对象发布和逸出

    单例模式

    懒汉模式:

    /**
     * 懒汉模式
     * 单例实例在第一次使用时进行创建
     */
    @NotThreadSafe
    public class SingletonExample1 {
    
        // 私有构造函数
        private SingletonExample1() {
    
        }
    
        // 单例对象
        private static SingletonExample1 instance = null;
    
        // 静态的工厂方法
        public static SingletonExample1 getInstance() {
            if (instance == null) {
                instance = new SingletonExample1();
            }
            return instance;
        }
    }
    这个是线程不安全的,如果同时有两个线程到达
        if(instance == null)
    一个new完之后还会再new一个对象
     
    为了使得具有线程安全性,可以使用synchronized关键字。
    /**
     * 懒汉模式
     * 单例实例在第一次使用时进行创建
     */
    @ThreadSafe
    @NotRecommend
    public class SingletonExample3 {
    
        // 私有构造函数
        private SingletonExample3() {
    
        }
    
        // 单例对象
        private static SingletonExample3 instance = null;
    
        // 静态的工厂方法
        public static synchronized SingletonExample3 getInstance() {
            if (instance == null) {
                instance = new SingletonExample3();
            }
            return instance;
        }
    }
    但是这种方式会使性能受影响。
    我们本来只是想让new这个操作并行就可以了,现在,只要是进入getInstance()的线程都得同步啊,
    注意,创建对象的动作只有一次,后面的动作全是读取那个成员变量,这些读取的动作不需要线程同步啊。
     

    改进:双重检测机制

    /**
     * 懒汉模式 -》 双重同步锁单例模式
     * 单例实例在第一次使用时进行创建
     */
    @NotThreadSafe
    public class SingletonExample4 {
    
        // 私有构造函数
        private SingletonExample4() {
    
        }
    
        // 1、memory = allocate() 分配对象的内存空间
        // 2、ctorInstance() 初始化对象
        // 3、instance = memory 设置instance指向刚分配的内存
    
        // JVM和cpu优化,发生了指令重排
    
        // 1、memory = allocate() 分配对象的内存空间
        // 3、instance = memory 设置instance指向刚分配的内存
        // 2、ctorInstance() 初始化对象
    
        // 单例对象
        private static SingletonExample4 instance = null;
    
        // 静态的工厂方法
        public static SingletonExample4 getInstance() {
            if (instance == null) { // 双重检测机制        // B
                synchronized (SingletonExample4.class) { // 同步锁
                    if (instance == null) {
                        instance = new SingletonExample4(); // A - 3
                    }
                }
            }
            return instance;
        }
    }

    所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

    但是这种方式依然是线程不安全的。原因是可能发生指令重排的情况。

    主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

    1. 给 singleton 分配内存
    2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
    3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

     
    进一步改进:使用volatile关键字,禁止进行指令重排(前面我们讲过,volatile的使用场景由两个,一个是状态表示量,一个是double check双重检测,也就是使用在这了)
    注意:
    volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,
    而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。
    /**
     * 懒汉模式 -》 双重同步锁单例模式
     * 单例实例在第一次使用时进行创建
     */
    @ThreadSafe
    public class SingletonExample5 {
    
        // 私有构造函数
        private SingletonExample5() {
    
        }
    
        // 1、memory = allocate() 分配对象的内存空间
        // 2、ctorInstance() 初始化对象
        // 3、instance = memory 设置instance指向刚分配的内存
    
        // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
        private volatile static SingletonExample5 instance = null;
    
        // 静态的工厂方法
        public static SingletonExample5 getInstance() {
            if (instance == null) { // 双重检测机制        // B
                synchronized (SingletonExample5.class) { // 同步锁
                    if (instance == null) {
                        instance = new SingletonExample5(); // A - 3
                    }
                }
            }
            return instance;
        }
    }
     
    饿汉模式:
    import com.mmall.concurrency.annoations.ThreadSafe;
    
    /**
     * 饿汉模式
     * 单例实例在类装载时进行创建
     */
    @ThreadSafe
    public class SingletonExample2 {
    
        // 私有构造函数
        private SingletonExample2() {
    
        }
    
        // 单例对象
        private static SingletonExample2 instance = new SingletonExample2();
    
        // 静态的工厂方法
        public static SingletonExample2 getInstance() {
            return instance;
        }
    }

    它是线程安全的。

    缺点 1.如果创建过程中进行很多的运算,会导致类加载的时候特别的慢

       2.如果创建出来的实例要很久以后才被调用,那么会导致资源的浪费

    前面是使用静态域 的方式,我们也可以使用静态代码块的方式:在写静态域和静态代码块的时候一定要注意他们之间的顺序,他们是按顺序执行的,顺序不同出现的结果也会不同

    /**
     * 饿汉模式
     * 单例实例在类装载时进行创建
     */
    @ThreadSafe
    public class SingletonExample6 {
    
        // 私有构造函数
        private SingletonExample6() {
    
        }
    
        // 单例对象,要放在静态代码块的前面
        private static SingletonExample6 instance = null;
    
        static {
            instance = new SingletonExample6();
        }
    
        // 静态的工厂方法
        public static SingletonExample6 getInstance() {
            return instance;
        }
    
        public static void main(String[] args) {
            System.out.println(getInstance().hashCode());
            System.out.println(getInstance().hashCode());
        }
    }

    Lazy initialization holder class模式

      这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

      1.相应的基础知识

    •  什么是类级内部类?

      简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

      类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

      类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

      类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

    •  多线程缺省同步锁的知识

      大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

      1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

      2.访问final字段时

      3.在创建线程之前创建对象时

      4.线程可以看见它将要处理的对象时

      2.解决方案的思路

      要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

      如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

      示例代码如下:

    public class Singleton {

    private Singleton(){}
    /**
    * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
    * 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
    */
    private static class SingletonHolder{
    /**
    * 静态初始化器,由JVM来保证线程安全
    */
    private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
    return SingletonHolder.instance;
    }
    }

      当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

      这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

    枚举模式

    /**
     * 枚举模式:最安全
     */
    @ThreadSafe
    @Recommend
    public class SingletonExample7 {
    
        // 私有构造函数
        private SingletonExample7() {
    
        }
    
        public static SingletonExample7 getInstance() {
            return Singleton.INSTANCE.getInstance();
        }
    
        private enum Singleton {
            INSTANCE;
    
            private SingletonExample7 singleton;
    
            // JVM保证这个方法绝对只调用一次
            Singleton() {
                singleton = new SingletonExample7();
            }
    
            public SingletonExample7 getInstance() {
                return singleton;
            }
        }
    }

     相比于懒汉模式更加容易保证安全性,相比饿汉模式只在需要使用的时候才执行初始化。

    Java 利用枚举实现单例模式

    这篇博客对java单例模式讲解的非常好:

    https://www.jianshu.com/p/eb30a388c5fc?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

    深入浅出单实例SINGLETON设计模式

  • 相关阅读:
    ActionResult 的返回类型
    MVC4.0中ViewBag、ViewData、TempData和ViewModel几种传值方式的区别
    抛出异常的区别 throw 和throw ex
    字段的重复校验问题
    bootstrap 动态添加验证项和取消验证项
    VS快捷键大全(转)
    数组、List和ArrayList的区别
    处理两个泛型集合差异化
    传参的两种方式
    tomcat中web项目编译后的结构
  • 原文地址:https://www.cnblogs.com/xiangkejin/p/9267372.html
Copyright © 2011-2022 走看看