Java并发_单例模式

概述


  • 先说下单例模式的实现,单例模式一般有 饿汉式懒汉式双重锁校验锁静态内部类枚举
  • 对于饿汉式,由于在类加载时期已经创建好单利对象,所以有且仅有一个对象,也不用考虑在多线程情况下会不会创建多个单利对象的问题。而对于懒汉式,由于延迟加载的原因,有可能导致实例多个对象等…
  • 单例模式中,有四大原则,下面也就通过这四大原则来探讨下面各种单例模式。
    • 构造私有。
    • 以静态方法或者枚举返回实例。
    • 确保实例只有一个,尤其是多线程环境。
    • 确保反序列换时不会重新构建对象。

1. 饿汉式


	class SingleTon {
        private static SingleTon instance = new SingleTon();

        //私有化构造器
        private SingleTon() {
        }

        public static SingleTon getInstance() {
            return instance;
        }
    }
  • 在类加载时期就创建好了单例对象,在后续使用中只会使用该对象,不存在多线程下的安全问题。
  • 优点: 在获取单例对象时,由于之前在类加载时期已经实例化好了,节省时间。
  • 缺点:由于在类加载时期创建对象,故在占用空间。

2. 传统懒汉式

	class SingleTon {
        /**
         * 在类加载时期不进行创建单例对象,在第一次使用时在进项创建。
         */
        private static SingleTon instance = null;

        private SingleTon() {
        }

        public static SingleTon getInstance() {
            //第一次使用instance对象为null,故应该创建单利对象
            if (instance == null) {
                instance = new SingleTon();
            }
            return instance;
        }
    }
  • 在类加载时期不进行创建单例对象,而在第一次使用时在进项创建。
  • 优点: 在不使用时,不创建对象,节省空间
  • 缺点 1: 在第一次使用时创建对象,导致第一个使用的人时间边长(这都不是事)
  • 缺点 2: 在多线程情况下不安全,获取到的可能不是同一个对象。(严重问题)

下面对懒汉式多线程下不安全问题进行测试:

	//在getInstance()方法中if判断中sleep 1毫秒
	public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"获取到的是\t"+ SingleTon.getInstance().hashCode());
            }, "线程" + i).start();
        }
    }

结果:

线程1获取到的是	1958819842
线程2获取到的是	785527010
线程3获取到的是	1027487819
线程4获取到的是	1699630862
线程0获取到的是	196683177
线程5获取到的是	196683177
线程6获取到的是	196683177
线程7获取到的是	196683177
线程8获取到的是	196683177
线程9获取到的是	196683177
  • 很容易发现前五个就不是同一个对象
  • 那出现这个问题的原因是啥呢??? 线程1进入 if 语句,还未来的及赋值,已经有其他线程也进入,等线程1赋值完毕,其它线程已经错过了判断为空的条件,它还在 if 中 所以 其他已经进入if语句中的线程又进行了一次创建和赋值。
    在这里插入图片描述

3. 双重校验锁(Double Check Lock)


  • 先不说双重校验锁是个什么,先解决一下上面的问题。
  • 懒汉式由于多线程条件下可能会有多个线程同时进入 if 语句块 并进行实例化,那么解决它可不可以使用 synchronized 同步方法,只允许一个线程进入该方法呢,答案是完全可以!!
  • 代码:
 	public synchronized static SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
  • 加了synchronized 完全可以解决多线程下的问题,因为有且仅有一个线程可以进入
  • 但是就因为第一创建的不安全,锁住整个方法,导致在赋值后还要进行单线程得进入方法获取
  • 这个方法也不是个好方法。所以 我们可以进行判断 如果已经赋值了就直接返回,没有赋值再进入同步代码块进行赋值。
  • 代码:
  	public static SingleTon getInstance() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                instance = new SingleTon();
            }
        }
        return instance;
    }
  • 这样的话还是有问题,比如 线程1,线程2,线程3 获取时这个对象还没创建出来,都是null,都同时进入if代码块中,在synchronized代码块中这三个线程依次执行,就初始化了三次。
    在这里插入图片描述
  • 所以,双重校验锁来啦
  • 在 synchronized 代码块中再判断一次 instance 是否为空即可,为空才创建,不为空直接跳过去过去就可以了
class SingleTon {
 	private static SingleTon instance = null;

    private SingleTon() {
    }
    
	public static SingleTon getInstance() {
        if (instance == null) {
           synchronized (SingleTon.class) {
                if(instance==null){
                	instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
  • 再次测试下:
线程0获取到的是	901590997
线程2获取到的是	901590997
线程8获取到的是	901590997
线程1获取到的是	901590997
线程9获取到的是	901590997
线程3获取到的是	901590997
线程5获取到的是	901590997
线程4获取到的是	901590997
线程6获取到的是	901590997
线程7获取到的是	901590997
  • 没毛病
  • 但是!!! 看起来没问题,但是还是有多线程下不安全因素存在。
  • 是因为在 instance = new SingleTon(); 这一句,JVM执行时是分解为三步来完成。
    ① 在堆内存开辟内存空间。
    ② 在堆内存中实例化SingleTon里面的各个参数。
    ③ 把对象指向堆内存空间。
  • 这三个步骤并没有任何的 数据依赖 关秀,所以这三个是可被重排序的,所以真正执行的可能是 ③ 比 ①②先执行。
  • 这样的执行顺序的话 一个线程先只想了③,然后被挂起了,另外一个线程进来,由于第一个线程已经执行了③,所以instance 已经不为null了,所以它直接返回,这样它就拿到的是一个未初始化完成的对象
  • 显然这样是不行的,那怎么解决,可以使用 volatile 修饰禁止指令重排序!!!
  • 下面是完整版的 DCL 单例模式代码
class SingleTon {
	// 使用 volatile 修饰禁止指令重排序,防止因为重排序导致的线程拿到没有初始化完成的对象
  	private static volatile SingleTon instance = null;

    private SingleTon() {
    }
    
	public static SingleTon getInstance() {
		//第一层校验: 是为了将锁的粒度化小,只是让还没有创建完成的进入去创建对象,已经创建完成的直接获取即可
        if (instance == null) {
           synchronized (SingleTon.class) {
           		//第二次校验:当多个线程一起进入上一层 if 判读,使用第二层可只让一个线程去实例化
                if(instance==null){
                	instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
  • 要是还进行优化的话,可以尝试一下下面这个

4. 静态内部类实现单例模式


	class SingleTon {
        private SingleTon() {
        }
        //静态内部类
        private static class SingleTonHoler {
            private static SingleTon instance = new SingleTon();
        }

        public static SingleTon getInstance() {
            return SingleTonHoler.instance;
        }
    }

要想弄明白静态内部类单例,先介绍个东西,类的加载时机:

  • 类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
    • 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时 (final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
    • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
    • 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
    • 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
  • 这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

  • 外部类加载时并不需要立即加载 内部类,内部类不被加载也就不会去初始化 instance,这样实现了懒汉式。
  • 即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化instance。
  • 调用getInstance()方法会导致虚拟机加载SingleTonHoler类,加载过程中初始化静态变量。

那么,它是怎么保证多线程情境下的安全性的呢??

  • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
  • 如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
  • 故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?

  • 其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

而且,在序列化和反序列化后返回的不是同一个对象(严重!!!!)

  public static void main(String[] args) {
        try {
            SingleTon serialize = SingleTon.getInstance();
            System.out.println(serialize.hashCode());
            
            //序列化
            FileOutputStream fo = new FileOutputStream("tem");
            ObjectOutputStream oo = new ObjectOutputStream(fo);
            oo.writeObject(serialize);
            oo.close();
            fo.close();
            //反序列化
            FileInputStream fi = new FileInputStream("tem");
            ObjectInputStream oi = new ObjectInputStream(fi);
            SingleTon serialize2 = (SingleTon) oi.readObject();
            oi.close();
            fi.close();
            
            System.out.println(serialize2.hashCode());
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class SingleTon implements Serializable {
        private SingleTon() {
        }

        //静态内部类
        private static class SingleTonHoler {
            private static SingleTon instance = new SingleTon();
        }

        public static SingleTon getInstance() {
            return SingleTonHoler.instance;
        }
    }

输出:

1956725890
931919113
  • 这显然就不是一个对象,解决方法添加readResolve方法,告诉反射,在获取对象时调用哪个方法。
	class SingleTon implements Serializable {
        private SingleTon() {
        }

        //静态内部类
        private static class SingleTonHoler {
            private static SingleTon instance = new SingleTon();
        }

        public static SingleTon getInstance() {
            return SingleTonHoler.instance;
        }

        //使用匿名内部类实现单例模式,在遇见序列化和反序列化的场景,得到的不是同一个实例
        //解决这个问题是在序列化的时候使用readResolve方法
        protected Object readResolve() {
            return SingleTonHoler.instance;
        }
    }

5. 枚举实现

 	public enum SingleTon{
        INSTANCE;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值