java中的7种单例模式

单例模式是我们开发中经常会用到的,单例模式的好处是应用在运行时只有一个实例,生命周期从单例实例化后一直到应用生命周期结束。这里总结和比较一下几种单例的形式,共总结了七种。

写法一

public class Singleton1 implements Serializable {private static Singleton1 instance = new Singleton1();private Singleton1(){}public static Singleton1 getInstance(){return instance;}
}

这种写法的问题是,在class文件装载虚拟机的时候,就会分配内存。 (这个说法有问题,已经在新的文章中解释:从两种单例模式谈java类加载过程中静态变量的初始化问题)有时候我们不希望应用一启动就有个单例占了内存,尤其在android这种内存敏感的嵌入式设备中,希望可以在使用的时候再去分配。但是这种写法是保证线程安全的,因为是在虚拟机加载class文件就分配的内存,虚拟机保证了单例的线程安全。

写法二

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

好,既然说Singleton1的写法不能实现延迟加载,那么就在获取单例的时候再初始化实例。但是这个写法有线程安全问题,详细见写法四。

写法三

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

你有张良计我有过墙梯。说我线程不安全?加个锁给你看看。在静态方法前加锁,锁住了这个类(class)对象(而不是实例)。但是synchronized锁是个重量级锁,当一个线程访问这个类,如果这个对象已经被另一个线程访问时,这个线程会一直阻塞。每次调用getInstance()方法都要阻塞,这样是很耗性能的。

写法四

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

这是一个经典的DCL(双重检查锁定)写法。获取单例时,如果当前实例为空,就对class对象加锁,加锁后为防止多线程在加锁期间生成单例,又做了一次判定,如果这时候实例还是空,创建。每次使用getInstance方法,如果实例存在,就直接返回。最多阻塞一次。
这种写法目前看起来很完美,但是问题来了,

if(instance == null){instance = new Singleton4();}

代码段和写法二中的一模一样。写法二有线程安全问题,代码四不是用synchroized处理了吗,还有线程安全的事?肯定有的,不然我不会费这力气说了。
在JMM(java内存模型)中,instance = new Singleton();可以分成几个指令:

  1. memory = allocate(memorySize);//分配一定大小的内存空间
  2. initMemory(memory);//初始化内存对象,比如初始化成员变量等等
  3. instance 指向 memory;//引用指向实例的地址

但是,了解JMM的童鞋应该知道,为了提高编译和运行效率,编译器和处理器会对代码做重排序(可以使指令对齐,处理器流水线更快),上述1-》2-》3的顺序可能会被重排成1-》3-》2,在单线程下,java语言规范保证了执行结果不变,但是在多线程下会出问题:

如图,A线程中,代码重排序成了1-》3-》2,在3执行之后,2之前,B线程访问instance,但是此时instance引用指向了分配的空间,但是这个空间并没有做实例对象的初始化工作!

这个问题的本质:

  1. 线程A中代码重排序了;
  2. 重排序就算了,在A没执行完全部代码之前,这个重排序让线程B看到了。

这个情况很少发生,但是真实发生的,这就悲催了。不过好在知道了什么原因,从根本入手解决。改进方案:

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

就是在静态变量前加了volatile关键字!四苦一,一个volatile就解决了?原来是在JDK5以及以上,volatile的语义被增强了。在编译器编译时,有volatile声明的变量会在前后插入插入屏障,保证不会在单一线程看来都不会重排序。也就是通过阻止本质1实现线程安全。

写法五

上面说了阻止本质1实现线程安全,现在讲一个阻止本质2的写法:

public class Singleton5 {private Singleton5(){}private static class InstanceHolder{private static Singleton5 instance = new Singleton5();}public static Singleton5 getInstance(){return InstanceHolder.instance;}
}

就是套了一个私有的静态内部类。java语言规范保证了,一个类或者接口的静态成员在被赋值的时候,这个类会初始化(有个初始化锁,每个线程都会至少获取一次初始化锁保证初始化),这个过程比较复杂,结果就是对任意线程,内部可以重排序,但是这种重排序对其他线程不可见。这个也是Google推荐的写法。

写法六

public enum  Singleton6 {INSTANCE;public void func(){}
}

黑科技式的写法。枚举类本质是继承了Enum类的静态类,但是enum会比普通静态变量更占内存,因为做了很多处理。JVM保证了static类的线程安全性。

写法七

public class Singleton7 {private static AtomicReference<Singleton7> instance = new AtomicReference<>();private Singleton7(){}public static Singleton7 getInstance(){for(;;){Singleton7 singleton7 = instance.get();if(singleton7!=null){return singleton7;}singleton7 = new Singleton7();if(instance.compareAndSet(null,singleton7)){return singleton7;}}}
}

高科技的写法。如果面试的时候让你写一种不用synchronized锁的线程安全的单例,这无疑是最佳答案(enum黑科技在JVM保证线程安全时一样用了synchronized)。这个写法用了CAS锁,CAS锁本质是用汇编cmpxchg指令实现锁cpu缓存。但是如果长时间获取不到锁,由于自旋(循环),会有较大的开销。

勘误线

“在class文件装载虚拟机的时候,就会分配内存。”这种说法是错的。是在类的初始化阶段完成的。

java中的7种单例模式相关推荐

  1. java中的单例_细说Java中的几种单例模式

    在Java中,单例模式分为很多种,本人所了解的单例模式有以下几种,如有不全还请大家留言指点: 饿汉式 懒汉式/Double check(双重检索) 静态内部类 枚举单例 一.饿汉式 image 饿汉式 ...

  2. java单例模式理解_快速理解Java中的五种单例模式

    解法一:只适合单线程环境(不好) packagetest;/***@authorxiaoping **/ public classSingleton {private static Singleton ...

  3. java中的复合数据类型是什么_【填空题】类是Java中的一种重要的复合数据类型,是组成Java程序的基本要素。一个类的实现包括两部分:____和_____....

    [填空题]类是Java中的一种重要的复合数据类型,是组成Java程序的基本要素.一个类的实现包括两部分:____和_____. 更多相关问题 [名词解释] 观叶树木 [单选] 开花时有浓郁香气的树种是 ...

  4. Java 中的四种引用

    垃圾收集器与内存分配策略参考目录: 1.判断Java 对象实例是否死亡 2. Java 中的四种引用 3.垃圾收集算法 4. Java9中的GC 调优 5.内存分配与回收策略 在进行垃圾回收之前,虚拟 ...

  5. 分析Java中的三种不同变量的区别

    1.首先分析Java中的三种不同变量的区别,如下表所示   概念 默认值 其他 类变量 也叫静态变量,是类中独立于方法之外的变量 用static 修饰 有默认初始值,系统自动初始化. 如boolean ...

  6. java中的五种排序方法_用Java排序的五种有用方法

    java中的五种排序方法 Java排序快速概述: 正常的列表: private static List VEGETABLES = Arrays.asList("apple", &q ...

  7. java类型转换答案,在java中支持两种类型的类型转换,自动类型转换和强制类型转换。父类转化为子类需要强制转换。...

    在java中支持两种类型的类型转换,自动类型转换和强制类型转换.父类转化为子类需要强制转换. 更多相关问题 计算机病毒通过()传染扩散得极快,危害最大. 当一个现象的数量由小变大,另一个现象的数量相反 ...

  8. Java中的两种异常类型及其区别?

    Java中的两种异常类型及其区别? 参考文章: (1)Java中的两种异常类型及其区别? (2)https://www.cnblogs.com/zxfei/p/11182730.html (3)htt ...

  9. <随笔03>Java中的两种异常类型

    <随笔03>Java中的两种异常类型 参考文章: (1)<随笔03>Java中的两种异常类型 (2)https://www.cnblogs.com/newlyfly/p/744 ...

最新文章

  1. 两个git库之间迁移_Python 3 迁移怨声载道
  2. 【OpenCV 4开发详解】方框滤波
  3. Winform中设置ZedGraph鼠标焦点位置画出十字线并在鼠标移出时十字线消失
  4. Asp.net中的常用路径
  5. java mysql geo_GEO数据库简介
  6. 【回归损失函数】L1(MAE)、L2(MSE)、Smooth L1 Loss详解
  7. LINUX命令之stat及显示的三个时间戳
  8. 对Java语言的byte类型变量进行无符号提升
  9. Photoshop裁剪工具隐藏技巧
  10. 编一个程序,将两个字符串连接起来,不要用strcat函数
  11. 计算机游戏 ppt背景图片,课件背景图片大全
  12. Java面向对象前奏:酒店客房管理系统
  13. Linux通用笔记---Kalrry
  14. 算法 - 随机密码生成算法
  15. java中小数后加f_在 Java 中,小数默认为 ,如果要指定 类型请在小数后加 F/f 。_学小易找答案...
  16. CSS设置超出几行显示省略号
  17. 最新H5游戏小游戏集成系统400多款趣味游戏
  18. 输出这个整数对应的拼音
  19. Android 程序锁
  20. python小课文件_Python--小甲鱼学习笔记--第28课:文件(文件打开方式、文件对象方法)...

热门文章

  1. vs登录界面空白_金蝶KIS云专业版登录使用时一片空白
  2. [前端面试]HTTP相关常问问题
  3. JS点击事件的使用方法
  4. 成为UX设计师:你需要知道的六个基本步骤
  5. SQL SERVER 中如何取年、月、日 -DATEPART函数
  6. spring事务隔离级别、传播行为以及spring+mybatis+atomikos实现分布式事务管理
  7. mysql gbk_MySQL字符集 GBK、GB2312、UTF8区别 解决MYSQL中文乱码问题
  8. 为行业赋能 助力行业客户业务大放异彩
  9. 【转】mysql命令提示符显示中文乱码或插入值均为空白
  10. 开源工作流自动化神器 n8n