Java 设计模式之单例模式


单例模式(Singleton Pattern)定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

1. 直接实例化【饿汉式】【很推荐使用】

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

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

2. 单一同步锁【懒汉式】【不推荐使用】

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

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

3. 双重校验锁【懒汉式】【不推荐使用】

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

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

volatile关键字保证线程修改的可见性

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

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

volatile关键字禁止指令重排序

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

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

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

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

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

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

4. 静态内部类【懒汉式】【很推荐使用】

静态内部类不会因为外部类加载而加载,只会在用到的时候加载。

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

5. 枚举实现单例【不常用】【不推荐使用】

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

        INSTANCE;

        private Singleton singleton;

        SingletonEnum() {
            singleton = new Singleton();
        }
        public Singleton getSingleton() {
            return singleton;
        }
    }
}

6. CAS实现单例【不常用】【不推荐使用】

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

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

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值