单例模式的优化之路(java)

时间:2023-03-10 05:47:12
单例模式的优化之路(java)

1.概述

  最近在优化公司以前老项目的代码时,发现有些类的代码频繁地创建和销毁对象,资源消耗比较严重。针对这些做了一些优化,改用单例模式,避免频繁的创建和销毁对象,说起单例模式,相信每个人都会写,接下来,我们来说下单例模式的优化。

2.优化

  单例模式,顾名思义就是只有一个实例,可以分为饿汉式和懒汉式;

  饿汉式:

public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
public class Singleton {
private static Singleton instance = null;
static{
instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

  优缺点分析:上述两种方式其实本质都是一样的,都是利用static来定义静态成员变量或静态代码。

        该种单例模式比较简单,在类加载的时候就已经创建好了实例,利用Class的类加载机制实现了线程安全单例

        但是这种模式的缺点就是在类加载的时候就已经完成了实例化,没有达到lazy-loading的效果,如果该实例始终没有用到,那么就会造成内存的浪费。

  饿汉式优化*1:

  

public class Singleton {
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.instance;
}
}

  优缺点分析:这种方式相比于前面两种有所优化,使用了lazy-loading。Singleton类被装载后并没有立即生成实例,当getInstance方法被调用时,才会装载SingletonHolder类,从而实例化instance。  

  单例优化:

public enum Singleton{
INSTANCE;
public void method(){
}
}

  优缺点分析:该方式是JDK1.5的时候加入的,不仅能避免多线程同步问题,还可以防止反序列化重新创建新的对象。

  上述几种方式,其实实现原理都是借助了JVM进行类加载的时候初始化单例,就是ClassLoader的线程安全机制。

  ClassLoader的线程安全机制就是指ClassLoader的loadClass方法在加载类的时候使用synchronized关键字。所以这就是为什么在类加载的过程中是线程安全的了。

  

  懒汉式:

public class Singleton(){
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if(null == instance){
singleton = new Singleton();
}
return singleton;
}
}

  优缺点分析:这种方式实现了lazy-loading,但是有一个明显的缺点就是,这种单例只能在单线程环境下使用,在多线程环境下,一个线程刚刚通过(null == instance)语句的同时,另一个线程也通过了该语句块,那么这个时候就会产生两个实例,这样就与单例模式只有一个实例的核心思想相悖了。所以在多线程模式不可使用该种方式。

  懒汉式优化*1:

public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

  优缺点分析:这种方式相比上面那种方法实现了线程同步,但是这个时候有个非常明显的缺点,就是效率低下,当调用getInstance()方法的时候都要进行同步,其实优化方式很明显,如果没有该实例,只需要在创建该实例的代码上添加synchronized代码块即可,若该实例已经存在,直接return该实例即可。

  懒汉式优化*2:

public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}

  优缺点分析:由于上面的方法同步效率太低,改用同步代码块的方法,但是该种方式根本不能起到线程同步的作用,因为由于实例化对象时,内存对象会进行重排序,就有可能会导致多线程的时候执行到if判断的时候还没被初始化或者得到一个不是null但是还未初始化完成的对象。

  懒汉式优化*3:

public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

  优缺点分析:这种Double-Check-Lock的方式进行了两次实例是否为空的判断,因为volatile 关键字就指定了禁止重排序,volatile保证了变量修改的可见性,但不保证原子性。一个线程对变量的修改,另一个线程能立即读到这个修改后的值,volatile是遵循happens-before原则的,这样多线程环境下,进行if判断的时候,能得到一个完整的已经实例好的对象,别的线程进行if判断的时候,直接返回该对象即可。这样我们就可以实现线程安全了,并且该种方式也实现了lazy-loading,效率高。

  那么有没有一种方式可以在不使用lock、synchronized的方式下实现线程安全的单例模式呢?答案是,有的,那就是使用CAS。

  CAS是什么呢?Compare And Swap,顾名思义就是比较和交换。CAS是项乐观锁技术,其包含三个参数,分别为V(待更新的值)、E(期望值)、N(新值),当V和E不相同时,说明其他线程已经做过更新了,此时该线程不执行更新操作,或者再次尝试读取V值再次尝试修改该值,也可以选择放弃该操作。若是V和E相等,则当前线程可以修改V值,也就是执行CAS操作。CAS操作中没有锁的参与,但是针对其他线程针对共享资源的操作做了处理。由于CAS中没有锁的参与,所以针对线程共享资源的操作也不会发生死锁了,可以说CAS天生免疫死锁。

public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton(){}
public static Singleton getInstance(){
for(;;){//不限次数的自旋循环,如果CAS一直失败,CPU的执行开销被消耗很严重
Singleton singleton = INSTANCE.get();
if(null != singleton){
return singleton;
}
singleton = new Singleton();
if(INSTANCE.compareAndSet(null, singleton)){//当前实例为null,才替换当前实例为singleton
return singleton;
}
}
}
}

  优缺点分析:该方式使用CAS实现线程安全,实现相比传统的锁机制来说,CAS依靠的是底层硬件(CPU的CAS指令)来实现的,不需要进行频繁的线程切换和阻塞而造成资源的额外消耗。

        但是这种方式还是有缺点的,CAS的自旋循环如果长时间不成功,则会给CPU带来非常大的执行开销。另外一点就是如果N个线程同时执行到singleton=new Singleton()的时候,则会同时创建大量的实例,很有可能发生OOM。

  CAS的缺点:首先CAS的ABA问题,这个可以通过添加版本号或时间戳来解决,在比较完内存中的值以后,再比较时间戳或者版本号是否一致。

        CAS的自旋操作,如果CAS长期不成功,会一直重试,会严重增加CPU的执行开销。JDK1.6以后默认开启了自旋(--XX:+UseSpinning),可以通过JVM设置CAS的自旋操作次数来解决(-XX:PreBlockSpin=10,JVM的默认自旋次数是10),当超过指定次数后,自动失败退出。还有一种自适应自旋锁,自旋的时间不再固定,会根据前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定的。

        CAS的功能的局限性,CAS只能保证单个内存中的值的原子性,在java中原子性不一定能保证线程安全,还需要volatile保证有序性来实现线程安全。在需要保证多个内存中的值的情况下,CAS也无能为力,可以看情况使用悲观锁。所以说在并发冲突概率比较高的环境中,尽量不要使用CAS。其次CAS的核心是依靠可以直接调用底层资源的Unsafe类的CompareAndSwap()方法实现的,在java使用只能使用Atomic包下的相关类,局限性比较大。

  

  参考文档:https://www.cnblogs.com/kismetv/p/10787228.html

相关文章