Java 实现设计模式之单例模式


单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。

应用场景:

  • 由于配置文件是共享的资源,配置对象的读取,一般应用单例模式。
  • 数据库连接池的设计一般也是采用单例模式。
  • 多线程的线程池的设计一般也是采用单例模式。

1. 饿汉式

饿汉式,类加载到内存后,就实例化一个单例,JVM 保证线程安全,简单实用,推荐使用。唯一缺点:不管用到与否,类装载时都会完成实例化(其实这个也不能算是缺点,因为加载它不就是为了用它吗?不然加载它干啥)。

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //静态实例私有化,防止外部引用,直接初始化。
    private static Singleton INSTANCE = new Singleton();
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
    
    
        return INSTANCE;
    }
}

2. 懒汉式

2.1. 单一同步锁

该方案性能太差,每次拿对象都得去获取锁,所以一般不推荐使用这种写法。

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //静态实例私有化,防止外部引用
    private static Singleton INSTANCE = null;
    //提供公开的静态方法,用来在外部获取实例
    public static synchronized Singleton getInstance(){
    
    
        if (INSTANCE == null){
    
    
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

2.2. 双重校验锁

DCL(Double Check Lock),双重校验锁。

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //静态实例私有化,防止外部引用(volatile 禁止指令重排,保证线程修改可见性)
    private static volatile Singleton INSTANCE = null;
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
    
    
        if (INSTANCE == null){
    
    //第一重校验
            synchronized (Singleton.class){
    
    //锁定
                if (INSTANCE == null){
    
    //第二重校验
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

2.2.1. volatile 的作用

(1)保证线程修改的可见性
Java 语言编写的程序,有时为了提高运行效率,编译器会自动对其优化,把经常访问的变量缓存起来,程序在读取这个变量时有可能直接从缓存(例如寄存器)中读取,而不会去内存中读取。当多线程编程时,变量的值可能因为被别的线程改变了,而该缓存的值不会相应的改变,从而造成读取的值与实际变量的值不一致。

volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

(2)禁止指令重排序
在 Java 内存模型(JMM)中,并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。

在执行INSTANCE = new Singleton();这条语句时,JMM 并不是一下就执行完毕的,即不是原子性的,实质上这句命令分为三大部分:

  1. 为对象分配内存空间,字段赋默认值。(申请空间)
  2. 执行构造方法语句,初始化实例对象。 (初始化对象)
  3. INSTANCE引用指向分配的内存空间。(引用关联)

在 JMM 中这三个步骤中的23不一定是顺序执行的,如果线程A执行的顺序为132,在第2步执行完毕的时候,恰好线程B执行第一次判空语句,则会直接返回INSTANCE,那么此时获取到的INSTANCE仅仅只是不为null,实质上没有初始化,这样的对象肯定是有问题的!(也就是说线程B可能获取到半初始化状态的对象,该对象内部的各字段的值都还是各类型的默认值,并未完成值的初始化。)

volatile关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!

2.2.2. volatile 的缺点

使用volatile屏蔽掉了 JVM 中必要的代码优化,所以在执行效率上会比较低。

2.3. 静态内部类

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //使用静态内部类维护单例
    private static class SingletonFactory{
    
    
        private static Singleton INSTANCE = new Singleton();
    }
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
    
    
        return SingletonFactory.INSTANCE;
    }
}

2.4. 枚举

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //使用枚举实现单例
    private enum Instance{
    
    
        INSTANCE;
        private Singleton singleton;
        Instance(){
    
    
            singleton = new Singleton();
        }
        public Singleton getSingleton(){
    
    
            return singleton;
        }
    }
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
    
    
        return Instance.INSTANCE.getSingleton();
    }
}

2.5. CAS

以上几种实现,其实现原理都是利用了类加载的时候初始化单例,即借助了 ClassLoader 的线程安全机制。所谓 ClassLoader 的线程安全机制,就是 ClassLoader 的 loadClass 方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。

所以,以上几种方法,虽然有的实现并没有显式的使用synchronized,但是其底层实现原理还是用到了synchronized

不使用锁的话,有办法实现线程安全的单例吗?有的,那就是使用CASCAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都会失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS(Compare And Swap)比较和替换是设计并发算法时用到的一种技术。

public class Singleton {
    
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){
    
    }
    //使用原子类包装
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
    
    
        for (;;){
    
    //死循环,相当于while(true){}
            Singleton singleton = INSTANCE.get();
            if (singleton != null){
    
    
                return singleton;
            }
            singleton = new Singleton();
            //CAS操作,如果INSTANCE为null,则把它修改为singleton
            if (INSTANCE.compareAndSet(null, singleton)){
    
    
                return singleton;
            }
        }
    }
}

这种方式实现的单例有啥优缺点吗?

CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。另外,如果 N 个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。

猜你喜欢

转载自blog.csdn.net/wb1046329430/article/details/115260999