一次性搞定所有单例模式

367 阅读5分钟

单例模式

保证该类只有一个实例,收回了实例的创建权限,通过一个对外接口提供实例对象。

使用场景

当创建一个对象需要消耗很多资源,或设计要求系统中该类只能有一个实例时使用该模式。

实现要点

  • 构造函数私有化
  • 通过静态方法或枚举返回单例对象,单例类中使用私有静态引用持有本对象
  • 确保在获取对象时是线程安全的
  • 确保反序列化时不会重新构建该对象

实践

1.饿汉模式

实现简单,但不能做到延迟加载

public class SingletonPattern1 implements Serializable {
    private static SingletonPattern1 instance = new SingletonPattern1();

    private SingletonPattern1() {
        // 防止反射获取多个对象的漏洞。
        if (null != instance) {
            throw new RuntimeException();
        }
    }

    public static SingletonPattern1 getInstance() {
        return instance;
    }

    // 防止反序列化导致多个对象的漏洞
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

2.懒汉模式

优化了饿汉不能延迟加载的问题,在获取对象时先判空再实例化。但在多线程下单例失效。

public class SingletonPattern2 implements Serializable {
    private static SingletonPattern2 instance;

    private SingletonPattern2() {
        if (null != instance) {
            throw new RuntimeException();
        }
    }

    public static SingletonPattern2 getInstance() {
        if (null == instance) {
            instance = new SingletonPattern2();
        }
        return instance;
    }

    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

3.同步懒汉模式

在判空实例化时进行同步,优化了普通懒汉在多线程下失效的问题。但每次获取不论实例是否为空都会进行同步操作,导致性能较低。

public class SingletonPattern3 implements Serializable {
    private static SingletonPattern3 instance;

    private SingletonPattern3() {
        if (null != instance) {
            throw new RuntimeException();
        }
    }

    /**
     * 对方法进行同步,解决多线程下实例不唯一的问题。
     *
     * @return
     */
    public synchronized SingletonPattern3 getInstance() {
        if (null == instance) {
            instance = new SingletonPattern3();
        }
        return instance;
    }

    /**
     * 使用同步代码块
     *
     * @return
     */
    public SingletonPattern3 getInstance1() {
        synchronized (SingletonPattern3.class) {
            if (null == instance) {
                instance = new SingletonPattern3();
            }
            return instance;
        }
    }

    private Object readResolve() throws ObjectStreamException {
        return instance;
    }

}

4.DCL 两步检查同步懒汉

使用两步检查解决了同步懒汉在实例不为空时仍需要同步的问题,使用violate避免了因JVM在对象实例化(new Obj 不是原子操作,存在优化重排序)过程中指令重排序导致模式失效的问题。

/**
 * DCl double check lock
 * 双检查锁机制,解决了在instance不为空时也要同步的缺陷,
 * 但是 new SingletonPattern4(); 不是原子操作,大致分为以下三个操作。
 * (1)在堆中划分内存
 * (2)调用构造函数进行初始化
 * (3)instance指向分配的内存空间
 * 正常是123,但是java 存在指令的重排序,导致出现132的情况,13完成时 另一个线程读取该对象时,就会导致获得一个未初始化的对象
 *
 * 使用volatile 就会避免该问题
 */
public class SingletonPattern4 implements Serializable {
    private static volatile SingletonPattern4 instance;

    private SingletonPattern4() {
        if (null != instance) {
            throw new RuntimeException();
        }
    }

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

    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

5.静态内部类模式

在私有的静态内部类内部中持有外部单例类的实例对象,获取实例对象时返回内部类中实例对象。没有使用synchronize,提高了性能,在第一次调用get方法时才会加载静态内部类,线程安全且全局唯一,也实现了延迟加载。

public class SingletonPattern5 implements Serializable {
    public static SingletonPattern5 getInstance() {
        return InstanceHolder.instance;
    }

    private SingletonPattern5() {
        if (null != InstanceHolder.instance) {
            throw new RuntimeException();
        }
    }

    private static class InstanceHolder {
        private static final SingletonPattern5 instance = new SingletonPattern5();
    }

    private Object readResolve() throws ObjectStreamException {
        return InstanceHolder.instance;
    }
}

6.枚举单例

枚举在java中和class一样,可以有字段也可以有方法,线程安全且能保持唯一。只有序列化存入磁盘,从磁盘反序列化得到新实例的时候会破坏唯一性。

public enum SingletonPattern6 {
    INSTANCE;
    public void doSomething(){
        System.out.println("do sth.");
    }
}

// 调用
public static void main(String[] args) {
    SingletonPattern6.INSTANCE.doSomething();
}

7.使用容器实现单例

public class SingletonPattern7 {
    private static Map<String, Object> objectMap = new HashMap<>();

    private SingletonPattern7() {
    }

    public static void registerService(String key, Object instance) {
        if (!objectMap.containsKey(key)) {
            objectMap.put(key, new SingletonPattern7());
        }
    }

    public static Object getService(String key) {
        return objectMap.get(key);
    }

}

以上单例模式其实在反序列化的时候都会破坏唯一性,因为反序列化时会通过一个特殊途径创建对象实例,相当于调用了私有构造函数。 可以通过加入readResolve方法,让开发人员控制对象的反序列化。

private Object readResolve() throws ObjectStreamException {
    return instance;
}

8.使用CAS实现单例模式

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。实现单例的方式如下:

public class Singleton {
    private static final AtomicReference INSTANCE = new AtomicReference();
    private Singleton() {}
    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

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

CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。

4. 总结

(建议用单例模式创建okHttpClient)OkHttpClient, 可以通过 new OkHttpClient() 或 new OkHttpClient.Builder() 来创建对象, 但是---特别注意, OkHttpClient() 对象最好是共享的, 建议使用单例模式创建。 因为每个 OkHttpClient 对象都管理自己独有的线程池和连接池。 这一点很多同学,甚至在我经历的团队中就有人踩过坑, 每一个请求都创建一个 OkHttpClient 导致内存爆掉

引用: www.jianshu.com/p/f3fae8658…