【Java】单例模式的五种实现方式以及为什么推荐枚举类
1. 饿汉式
构造私有 静态成员 提供方法获取
public class SingleTarget {
private static SingleTarget instance = new SingleTarget();
private SingleTarget(){};
public static SingleTarget getInstance()
{
return instance;
}
}
测试一下
public static void main(String[] args) {
SingleTarget instance1 = SingleTarget.getInstance();
SingleTarget instance2 = SingleTarget.getInstance();
System.out.println( instance1 == instance2);
}
由于 是静态的 不管你有没有调用方法去拿这个实例,他也会先加载到内存中
同时,还能通过反射创建出多个对象
//反射创建单例对象
Class<SingleTarget> aClass = (Class<SingleTarget>) Class.forName("单例模式.饿汉式.SingleTarget");
//获取构造方法对象
Constructor<SingleTarget> constructor = aClass.getDeclaredConstructor();
//开启暴力反射
constructor.setAccessible(true);
//调用构造方法创建对象
SingleTarget instance2 = constructor.newInstance();
SingleTarget instance1 = SingleTarget.getInstance();
System.out.println( instance1 == instance2);
2. 懒汉式
懒汉式解决了饿汉式不能懒加载这么一个问题, 但也存在反射创建多个对象这么一个问题 且 是线程不安全的
public class SingleTarget {
private static SingleTarget instance;
private SingleTarget() {
}
;
public static SingleTarget getInstance() {
if (null == instance) {
instance = new SingleTarget();
}
return instance;
}
}
反射在饿汉式中已经演示过了 这里就不在演示了 演示下线程安全问题
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(16);
IntStream.rangeClosed(0,100).parallel().forEach(i->{
executorService.submit(()->{
System.out.println("i = " + i);
SingleTarget instance1 = SingleTarget.getInstance();
SingleTarget instance2 = SingleTarget.getInstance();
if (instance1 != instance2){
System.out.println("出现线程安全问题");
System.out.println("instance1 = " + instance1);
System.out.println("instance2 = " + instance2);
}
});
});
//SingleTarget instance1 = SingleTarget.getInstance();
//SingleTarget instance2 = SingleTarget.getInstance();
//
//System.out.println( instance1 == instance2);
}
}
为什么会有线程安全问题呢 ? 看我们的代码 当两个线程同时走到这一步的时候,都会进if 结果 创建了两个对象 你可能会说,那加锁啊
是的 加锁 因此有了双检锁模式
3. 双检锁模式
懒汉式存在线程安全问题, 是的 加锁能解决一切线程安全问题 但是要考虑效率问题 锁加在什么地方合适呢?
不管是不是第一次获取实例,都得等一下 效率非常低 怎么样分流呢 ?
改成这样, 咋一看好像没什么问题 但是同样的这还是存在线程安全问题
如果首次调用的时候多个线程同时走到 if 判断 而此时 instance 确实是 null, 那么都会进入 if 虽然同时只能一个线程去new 这个对象,但是后面的线程也能执行 所以这样写时有问题的 应该要改成这样
public class SingleTarget {
private static SingleTarget instance;
private SingleTarget() {};
public static SingleTarget getInstance() {
if (null == instance) {
synchronized (SingleTarget.class){
if (null == instance) {
instance = new SingleTarget();
}
}
}
return instance;
}
}
那改成这样是不是也没问题了 ? 其实这里还存在一个小问题 由于
instance = new SingleTarget();
并不是一个原子操作 可能会存在指令重排 所以最好是加上 volatile 关键字
public class SingleTarget {
private static volatile SingleTarget instance;
private SingleTarget() {};
public static SingleTarget getInstance() {
if (null == instance) {
synchronized (SingleTarget.class){
if (null == instance) {
instance = new SingleTarget();
}
}
}
return instance;
}
}
双检锁模式下 又是加锁又是双重检查 特别麻烦 并且也能通过反射创建多个对象
所以我们看下一种实现方法
4. 静态内部类
public class SingleTarget {
private SingleTarget() {};
public static class SingleTargetUtilClass{
private static final SingleTarget instance = new SingleTarget();
}
public static SingleTarget getInstance()
{
return SingleTargetUtilClass.instance;
}
}
与双检锁模式相比 更加简单,同时也不存在线程安全问题也能做到懒加载
但是也存在线程安全问题
有没有十全十美的方式呢 有 枚举
5. 枚举类
public enum SingleTarget {
INSTANCE;
public void doAny()
{
System.out.println("做你想做");
}
}
如果你想通过反射创建枚举类的实例
Constructor<SingleTarget> declaredConstructor = SingleTarget.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
SingleTarget singleTarget = declaredConstructor.newInstance();
会给你抛出异常
Cannot reflectively create enum objects
`newInstance 方法 会进行判断 如果是枚举类 直接抛出异常
或许你用的最多的是饿汉式去实现单例模式 但其实你可以尝试着用枚举类去实现一下 推荐 静态内部类和枚举类