Java多线程编程读书笔记(二)

本文为多线程编程核心技术第6章,单例模式与多线程
跳过中间章节来进行本章节的知识总结,其原因在于面试中被面试官问了一个看似简单的问题,如何创建单例模式,这个当然很简单,但是当面试官问到如何创建一个安全的单例模式时,一脸懵逼。。。。so,面试结束赶快充电,暂定为这样理解题目……..
本章核心内容:如何使单例模式在遇到多线程时是安全的,正确的!!!
一、单例模式
①.立即加载/饿汉模式
理解:立即加载就是使用类时,已经将对象创建完毕。也就是说立即加载在调用方法前,实例已经被创建了

public class FastSingleton {
    //立即模式,在调用getSingleton()方法前实例已经创建
    private static FastSingleton fastSingleton = new FastSingleton();
    private FastSingleton(){
    }

    public static FastSingleton getSingleton() {
        return fastSingleton;
    }
}

因为构造函数是私有的,所以立即加载可以确保在多线程的环境下不会出现多个实例的错误,但是,静态实例会在类加载的时候就会被创建,如果该实例占用内存较大,或者在某个特定场景才会用到时,就不适合使用该方式创建单例,应使用延迟加载的方式。
②、延迟加载/懒汉模式
延迟加载就是在调用get()方法时,实例才被创建,常见的方法就是在get()中进行new实例化

public class LazySingleton {
    private static LazySingleton lazySingleton;
    private LazySingleton(){
    }

    public static LazySingleton getSingleton() {
        //延迟加载
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

虽然立即加载和延迟加载都可以实现获取一个实例,但是在多线程的环境下,延迟加载就会出现问题,因为多线程可能会同步调用get(),导致多个实例被创建。

二、解决方法:
①、声明Synchronized关键字,对get()加锁,但是变为同步方法会极大的降低效率,因为下一个线程想要取得对象,就必须等上一个线程释放锁,才可以继续

synchronized public  static  LazySingleton getSingleton() {
        try {
            //延迟加载
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return lazySingleton;
    }

②、同步代码块
和①同样的缺点:效率低,因为还是同步的方式

public  static  LazySingleton getSingleton() {
        try {
            //延迟加载
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        return lazySingleton;
    }

③、同步重要代码
在多线程时会由于并发而出现错误。

 public  static  LazySingleton getSingleton() {
    try {
        //延迟加载
            if (lazySingleton == null) {
                synchronized (LazySingleton.class) {
                    lazySingleton = new LazySingleton();
                }
            }
    }catch (Exception e){
        e.printStackTrace();
    }

    return lazySingleton;
}

④、使用DCL双检查锁(Double-Check-Lock)机制

public class LazySingletonDCL {
    private volatile static LazySingletonDCL lazySingletonDCL;
    private LazySingletonDCL(){
    }
    public static LazySingletonDCL getLazySingleton(){
        try{
            if (lazySingletonDCL == null){
                Thread.sleep(3000);
                synchronized (LazySingletonDCL.class){
                    if (lazySingletonDCL == null){
                        lazySingletonDCL = new LazySingletonDCL();
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return lazySingletonDCL;
    }
}

解释下为什么被称为双检查锁:
首先,在声明实例时,使用了volatile关键字,而volatile关键字的作用就是线程在对同一个变量值取值时,强迫其在共享内存中重新取值,而任何对成员变量进行修改的操作结果,都会被强迫写回共享内存中。也就是说,无论多少个线程,他们看到的都是同一个变量值。这就是volatile变化可见的功能。
在java中,允许线程保存共享内存中变量的私有拷贝,只有在离开或进入到同步代码块时才会和共享内存中进行值比较。所以,使用volatile也会降低效率
在DCL机制中,当进入synchronized代码块时,会进行一次值比较的操作,所以不会出现③的错误。

PS:在大多数的延迟加载的环境中,都会使用DCL机制解决线程不安全的问题!

三、其他的解决方案:
①、静态内部类:

public class SingletonStaticInnerClass {
    //利用类加载机制实现,和立即加载原理相同,但是当不使用内部类时,实例不会被创建
    //实现了延迟加载和线程安全
    private static class SingletonStaticInnerClassHandler{
        private static SingletonStaticInnerClass singletonStaticInnerClass = new SingletonStaticInnerClass();
    }
    private SingletonStaticInnerClass(){
    }
    public static SingletonStaticInnerClass getInstance(){
        return SingletonStaticInnerClassHandler.singletonStaticInnerClass;
    }
}

②、static代码块

public class SingletonStaticBlock {
    private static SingletonStaticBlock singletonStaticBlock=null;
    private SingletonStaticBlock(){}
    //静态代码块中的代码在使用类时就已经被执行
    static {
        singletonStaticBlock = new SingletonStaticBlock();
    }
    public static SingletonStaticBlock getInstance(){
        return singletonStaticBlock;
    }
}

③、序列化与反序列化的单例实现
使用内部静态类可以实现安全单例,但遇到序列化对象时,使用默认的方式运行得到的还是多例的
解决办法:在反序列化时使用readResolve()

public class SingletonSerial implements Serializable{
    private static final long serialVersion = 888L;
    private static class SingletonSerialHandler{
        private static final SingletonSerial singletonSerial = new SingletonSerial();
    }
    private SingletonSerial(){}
    public static SingletonSerial getInstance(){
        return SingletonSerialHandler.singletonSerial;
    }
    //使用readResolve()
    protected Object readResolve() throws ObjectStreamException{
        return SingletonSerialHandler.singletonSerial;
    }
}

在反序列化时抛出ObjectStreamException异常

④、使用枚举类实现
JDK1.5以后出现了枚举
枚举类:
枚举的本质是类,有自己的成员变量,方法和构造函数

public enum EnumClass {
    A("a",1),
    B("b",2);

    private String name;
    private int index;
    private EnumClass(String name,int index){
        this.name= name;
        this.index = index;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

}

枚举屏蔽了枚举值的类型信息,不像在用public static final定义常量必须指定类型
枚举是用来构建常量数据结构的模板,这个模板可扩展。枚举的使用增强了程序的健壮性。
我们可以将相关的常量分配到一个枚举类中

通过枚举类来创建一个线程安全的单例模式是十分简单的

class Resource{
    //需要使用的资源
}

public enum SingletonEnum {
    INSTANCE;
    private Resource instance;
    SingletonEnum(){
        instance = new Resource();
    }
    public Resource getInstance(){
        return instance;
    }
}

获取资源的方式很简单,通过Singleton.INSTANCE.getInstance()即可获得单例。
保证单例:
构造函数的私有特性,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
最重要的是,enum类拥有完善的序列化的机制,可以防止反序列化时的多例出现!
此处感谢Java 利用枚举实现单例模式的详细讲解!

多种设计模式用的最多的算是单例了,但是之前很少会考虑单例的不安全性,希望以该问题引以为戒,学习知识多纵向思考,深入挖掘!

猜你喜欢

转载自blog.csdn.net/qzy313885531/article/details/79134575