萌萌の初音
萌萌の初音
发布于 2022-03-20 / 478 阅读
0

Java下的7种单例模式以及使用场景、线程安全

单例模式分为5种:饿汉模式、懒汉模式、线程安全的懒汉模式、双重校验锁模式、静态内部类模式、枚举单例、原子应用类单例。

  • 饿汉模式实现
public class TestDemo {
    private static TestDemo instance=new TestDemo();
    private TestDemo() {}
    public static TestDemo getInstance() {
        return instance;
    }
}

  • 懒汉模式实现
public class TestDemo {
    private static TestDemo instance;
    private TestDemo() {}
    public static TestDemo getInstance() {
        if(instance == null){
            instance = new TestDemo();
        }
        return instance;
    }
}
  • 线程安全的懒汉模式
public class TestDemo {
    private static TestDemo instance;
    private TestDemo(){}
    public static synchronized TestDemo getInstance() {
        if(instance == null){
            instance = new TestDemo();
        }
        return instance;
    }
}
  • 双重校验锁模式
public class TestDemo {
    private volatile static TestDemo instance;
    private TestDemo() {} 
    public static TestDemo getInstance() {
        if(instance == null) {
            synchronized (TestDemo.class) {
                if(instance == null) {
                    instance = new TestDemo();
                }
            }
        }
        return instance;
    }
}
  • 静态内部类模式
public class TestDemo {
    private static class TestBuilder {
        private static TestDemo instance = new TestDemo();
    }
    private TestDemo() {}
    public static TestDemo getInstance(){
        return TestBuilder.instance;
    }
}

介绍了5种单例的实现,那么什么时候使用单例模式呢?

应用场景

1.整个程序调用这个类只允许有一个实例。
2.需要频繁实例化然后销毁的对象。
3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
4.方便资源相互通信的环境

使用场景
  1. 在项目封装的工具类中实现,保证项目常用的数据统一(如:图片加载、文件复制删除、文件下载、音频播放等)。
  2. 保存数据在内存中,方便其他类的读取(如:获取到token创建为static静态类方便网络请求类的读取)。

使用的优缺点

优点

1.实现了整个程序对唯一实例访问的控制。
2.因为单例要求程序只能有一个对象,所以对于那些需要频繁创建和销毁的对象来说可以提高系统的性能,并且可以节省内存空间。
3.可以全局访问。
4.允许可变数目的实例。

缺点

1.不适用于变化频繁的对象。
2.符合的场景有限。
3.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,可能会导致对象状态的丢失。
4.可扩展性较差。

在多线程情况下使用懒汉模式会导致实例多次创建,通过对懒汉模式获取实例的方法进行加锁解决(线程安全的懒汉模式),但线程安全的懒汉模式在有一个线程访问的情况下其他进程会导致挂起,导致性能问题;而双重校验锁模式的volatile可以禁止jvm指令重排,使多线程环境下使用保证正常;

什么是重排?

instance = new TestDemo();

在这段实例代码中分为了三个步骤:

  1. instance分配内存空间
  2. 初始化instance
  3. 将instance指向分配的内存地址
    正常情况下这三个步骤都会依次执行,但由于jvm的重排会导致1-3-2的情况发生,多线程情况下使用就会造成部分线程获取没有初始化的实例(假如a线程第一次创建TestDemo,线程b判断instance不为null就直接返回)。所以在双重校验锁模式中使用volatile的原因。
  • 补充:枚举单例
public enum TestDemo {
    INSTANCE;
    public void test() {}
}

使用:

TestDemo.INSTANCE.test();

枚举实例创建都是线程安全的,在任何情况下都是单例。

  • 补充:反序列化
    通过反序列化以上单例模式会重新创建对象,可以通过readResolve方法对反序列化进行控制。加入以下方法可以杜绝反序列化重新生成对象:
private Object readResolve() throws ObjectStreamException {
    return instance;
}
  • 补充:AtomicReference原子应用类单例
public class TestDemo {
    private static final AtomicReference<TestDemo> INSTANCE = new AtomicReference();

    private TestDemo() {}

    public static TestDemo getInstance() {
        for (;;) {
            TestDemo current = INSTANCE.get();
            if (current != null) return current;
            current = new TestDemo();
            if (INSTANCE.compareAndSet(null, current)) return current;
        }
    }
}

current 为volatile类型,保证某一个线程修改值时,其他线程的值都是最新的。
compareAndSet 设置值的操作是原子的,保证线程操作值时不被中断。compareAndSet 是无阻塞的锁,性能损耗较小。
原子应用类单例能够保证延迟加载也能保证原子性及实例的唯一性;原子变量可以避免优先级倒置和锁死等风险。
参考自SilenceDut博客