为了对比几种单例模式在多线程下的实际性能,我编写了一个测试类。

在这个测试类中,我使用了8线程的并发测试。每个线程分别对其进行 重复 1000000 次的获取实例对象操作,最终计算耗时并输出到控制台。

你可以通过注释/取消注释 MyThread 类中的 run 方法来测试不同单例设计模式的性能。

以下性能测试的分析,都来自我本机的测试情况。

测试类

SingletonTester

  1. import java.util.HashSet;
  2. /**
  3. * 单例模式性能测试类
  4. */
  5. public class SingletonTester {
  6. public static void main(String[] args) {
  7. MyThread thread1 = new MyThread();
  8. MyThread thread2 = new MyThread();
  9. MyThread thread3 = new MyThread();
  10. MyThread thread4 = new MyThread();
  11. MyThread thread5 = new MyThread();
  12. MyThread thread6 = new MyThread();
  13. MyThread thread7 = new MyThread();
  14. MyThread thread8 = new MyThread();
  15. thread1.setName("thread1");
  16. thread2.setName("thread2");
  17. thread3.setName("thread3");
  18. thread4.setName("thread4");
  19. thread5.setName("thread5");
  20. thread6.setName("thread6");
  21. thread7.setName("thread7");
  22. thread8.setName("thread8");
  23. thread1.start();
  24. thread2.start();
  25. thread3.start();
  26. thread4.start();
  27. thread5.start();
  28. thread6.start();
  29. thread7.start();
  30. thread8.start();
  31. }
  32. }
  33. /**
  34. * 线程测试类
  35. */
  36. class MyThread extends Thread {
  37. @Override
  38. public void run() {
  39. // 饿汉模式、线程安全
  40. // Singleton();
  41. // 懒汉模式、线程不安全
  42. // Singleton_1();
  43. // 懒汉模式 + synchronized同步锁、线程安全
  44. Singleton_2();
  45. // 懒汉模式 + synchronized同步锁(移到方法体内) 、线程不安全 。( 为了优化 Singleton_2 的性能问题改进的版本1)
  46. // Singleton_3();
  47. // 懒汉模式 + synchronized同步锁 + double-check 、线程可能不安全(指令重排序)。( 为了解决 Singleton_3 带来的线程不安全问题 )
  48. // Singleton_4();
  49. // 懒汉模式 + synchronized同步锁 + double-check + volatile 线程安全。( 为了解决 Singleton_4 因为指令重排序可能导致的线程不安全问题 )
  50. // Singleton_5();
  51. // 懒汉模式 内部类实现
  52. // Singleton_6();
  53. // 枚举方式
  54. // Singleton_7();
  55. }
  56. /**
  57. * Singleton
  58. */
  59. public void Singleton() {
  60. long time1 = System.currentTimeMillis();
  61. HashSet map = new HashSet();
  62. for (int i = 0; i < 1000000; i++) {
  63. boolean e = map.add(Singleton.getInstance());
  64. if (e && i != 0) {
  65. System.out.println("Singleton 获取到的对象不相等,非线程安全。");
  66. break;
  67. }
  68. }
  69. long time2 = System.currentTimeMillis();
  70. System.out.println(this.getName() + "线程 Singleton 测试耗时:" + (time2 - time1) + "ms");
  71. }
  72. /**
  73. * Singleton_1
  74. */
  75. public void Singleton_1() {
  76. long time1 = System.currentTimeMillis();
  77. HashSet map = new HashSet();
  78. for (int i = 0; i < 1000000; i++) {
  79. boolean e = map.add(Singleton_1.getInstance());
  80. if (e && i != 0) {
  81. System.out.println("Singleton_1 获取到的对象不相等,非线程安全。");
  82. break;
  83. }
  84. }
  85. long time2 = System.currentTimeMillis();
  86. System.out.println(this.getName() + "线程 Singleton_1 测试耗时:" + (time2 - time1) + "ms");
  87. }
  88. /**
  89. * Singleton_2
  90. */
  91. public void Singleton_2() {
  92. long time1 = System.currentTimeMillis();
  93. HashSet map = new HashSet();
  94. for (int i = 0; i < 1000000; i++) {
  95. boolean e = map.add(Singleton_2.getInstance());
  96. if (e && i != 0) {
  97. System.out.println("Singleton_2 获取到的对象不相等,非线程安全。");
  98. break;
  99. }
  100. }
  101. long time2 = System.currentTimeMillis();
  102. System.out.println(this.getName() + "线程 Singleton_2 测试耗时:" + (time2 - time1) + "ms");
  103. }
  104. /**
  105. * Singleton_3
  106. */
  107. public void Singleton_3() {
  108. long time1 = System.currentTimeMillis();
  109. HashSet map = new HashSet();
  110. for (int i = 0; i < 1000000; i++) {
  111. boolean e = map.add(Singleton_3.getInstance());
  112. if (e && i != 0) {
  113. System.out.println("Singleton_3 获取到的对象不相等,非线程安全。");
  114. break;
  115. }
  116. }
  117. long time2 = System.currentTimeMillis();
  118. System.out.println(this.getName() + "线程 Singleton_3 测试耗时:" + (time2 - time1) + "ms");
  119. }
  120. /**
  121. * Singleton_4
  122. */
  123. public void Singleton_4() {
  124. long time1 = System.currentTimeMillis();
  125. HashSet map = new HashSet();
  126. for (int i = 0; i < 1000000; i++) {
  127. boolean e = map.add(Singleton_4.getInstance());
  128. if (e && i != 0) {
  129. System.out.println("Singleton_4 获取到的对象不相等,非线程安全。");
  130. break;
  131. }
  132. }
  133. long time2 = System.currentTimeMillis();
  134. System.out.println(this.getName() + "线程 Singleton_4 测试耗时:" + (time2 - time1) + "ms");
  135. }
  136. /**
  137. * Singleton_5
  138. */
  139. public void Singleton_5() {
  140. long time1 = System.currentTimeMillis();
  141. HashSet map = new HashSet();
  142. for (int i = 0; i < 1000000; i++) {
  143. boolean e = map.add(Singleton_5.getInstance());
  144. if (e && i != 0) {
  145. System.out.println("Singleton_5 获取到的对象不相等,非线程安全。");
  146. break;
  147. }
  148. }
  149. long time2 = System.currentTimeMillis();
  150. System.out.println(this.getName() + "线程 Singleton_5 测试耗时:" + (time2 - time1) + "ms");
  151. }
  152. /**
  153. * Singleton_6
  154. */
  155. public void Singleton_6() {
  156. long time1 = System.currentTimeMillis();
  157. HashSet map = new HashSet();
  158. for (int i = 0; i < 1000000; i++) {
  159. boolean e = map.add(Singleton_6.getInstance());
  160. if (e && i != 0) {
  161. System.out.println("Singleton_6 获取到的对象不相等,非线程安全。");
  162. break;
  163. }
  164. }
  165. long time2 = System.currentTimeMillis();
  166. System.out.println(this.getName() + "线程 Singleton_6 测试耗时:" + (time2 - time1) + "ms");
  167. }
  168. /**
  169. * Singleton_7
  170. */
  171. public void Singleton_7() {
  172. long time1 = System.currentTimeMillis();
  173. HashSet map = new HashSet();
  174. for (int i = 0; i < 1000000; i++) {
  175. boolean e = map.add(Singleton_7.INSTANCE);
  176. if (e && i != 0) {
  177. System.out.println("Singleton_7 获取到的对象不相等,非线程安全。");
  178. break;
  179. }
  180. }
  181. long time2 = System.currentTimeMillis();
  182. System.out.println(this.getName() + "线程 Singleton_7 测试耗时:" + (time2 - time1) + "ms");
  183. }
  184. }
  185. /**
  186. * 饿汉模式、线程安全
  187. * 这种方式实现的单例模式,在类初始化阶段就已经在堆内存中开辟了一块内存,用于存放实例化对象,所以也称为饿汉模式。饿汉模式实现的单例的优点是,可以保证多线程情况下实例的唯一性,而且 getInstance 直接返回唯一实例,性能非常高。
  188. * 然而,在类成员变量比较多,或变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存。试想下,如果一个第三方开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,无疑是灾难性的。
  189. */
  190. class Singleton {
  191. private static Singleton instance = new Singleton();
  192. private Singleton() {
  193. }
  194. public static Singleton getInstance() {
  195. return instance;
  196. }
  197. }
  198. /**
  199. * 懒汉模式、线程不安全
  200. * 懒汉模式就是为了避免直接加载类对象时提前创建对象的一种单例设计模式。该模式使用懒加载方式,只有当系统使用到类对象时,才会将实例加载到堆内存中
  201. */
  202. class Singleton_1 {
  203. private static Singleton_1 instance = null;
  204. private Singleton_1() {
  205. }
  206. public static Singleton_1 getInstance() {
  207. if (null == instance) {
  208. instance = new Singleton_1();
  209. }
  210. return instance;
  211. }
  212. }
  213. /**
  214. * 懒汉模式 + synchronized同步锁、线程安全
  215. * 同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能。
  216. */
  217. class Singleton_2 {
  218. private static Singleton_2 instance = null;
  219. private Singleton_2() {
  220. }
  221. public static synchronized Singleton_2 getInstance() {
  222. if (null == instance) {
  223. instance = new Singleton_2();
  224. }
  225. return instance;
  226. }
  227. }
  228. /**
  229. * 懒汉模式 + synchronized同步锁 、线程不安全
  230. * 每次请求获取类对象时,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。基于以上两点,我们可以考虑将同步锁放在 if 条件里面,这样就可以减少同步锁资源竞争。
  231. */
  232. class Singleton_3 {
  233. private static Singleton_3 instance = null;
  234. private Singleton_3() {
  235. }
  236. public static Singleton_3 getInstance() {
  237. if (null == instance) {
  238. synchronized (Singleton_3.class) {
  239. instance = new Singleton_3();
  240. }
  241. }
  242. return instance;
  243. }
  244. }
  245. /**
  246. * 懒汉模式 + synchronized同步锁 + double-check 、线程可能不安全
  247. * 这种方式,通常被称为 Double-Check,它可以大大提高支持多线程的懒汉模式的运行性能。但有可能因为 JVM 编译时的重排序而造成线程不安全。
  248. */
  249. class Singleton_4 {
  250. private static Singleton_4 instance = null;
  251. private Singleton_4() {
  252. }
  253. public static Singleton_4 getInstance() {
  254. if (null == instance) {
  255. synchronized (Singleton_4.class) {
  256. if (null == instance) {
  257. instance = new Singleton_4();
  258. }
  259. }
  260. }
  261. return instance;
  262. }
  263. }
  264. /**
  265. * 懒汉模式 + synchronized同步锁 + double-check + volatile 线程安全
  266. */
  267. class Singleton_5 {
  268. private volatile static Singleton_5 instance = null;
  269. private Singleton_5() {
  270. }
  271. public static Singleton_5 getInstance() {
  272. if (null == instance) {
  273. synchronized (Singleton_5.class) {
  274. if (null == instance) {
  275. instance = new Singleton_5();
  276. }
  277. }
  278. }
  279. return instance;
  280. }
  281. }
  282. /**
  283. * 懒汉模式 内部类实现
  284. */
  285. class Singleton_6 {
  286. private static Singleton_5 instance = null;
  287. private Singleton_6() {
  288. }
  289. // 内部类实现
  290. public static class InnerSingleton {
  291. private static Singleton_6 instance = new Singleton_6();
  292. }
  293. public static Singleton_6 getInstance() {
  294. return InnerSingleton.instance;
  295. }
  296. }
  297. /**
  298. * 枚举方式
  299. */
  300. enum Singleton_7 {
  301. //实例
  302. INSTANCE;
  303. public static Singleton_7 getInstance() {
  304. return INSTANCE;
  305. }
  306. public void doSomething() {
  307. System.out.println("doSomething");
  308. }
  309. }

在这个测试类中,我使用了8线程的并发测试。每个线程分别对其进行 重复 1000000 次的获取实例对象操作,最终计算耗时并输出到控制台。

你可以通过注释/取消注释 MyThread 类中的 run 方法来测试不同单例设计模式的性能。

以下性能测试的分析,都来自我本机的测试情况。

Singleton

Singleton 类:饿汉模式、线程安全。

饿汉模式实现的单例的优点是,可以保证多线程情况下实例的唯一性,而且 getInstance 直接返回唯一实例,性能非常高。然而,在类成员变量比较多,或变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存。试想下,如果一个第三方开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,无疑是灾难性的。

  1. class Singleton {
  2. private static Singleton instance = new Singleton();
  3. private Singleton() {
  4. }
  5. public static Singleton getInstance() {
  6. return instance;
  7. }
  8. }

image.png
30~50 ms

Singleton_1

Singleton_1 类:懒汉模式、线程不安全

懒汉模式就是为了避免直接加载类对象时提前创建对象的一种单例设计模式。该模式使用懒加载方式,只有当系统使用到类对象时,才会将实例加载到堆内存中。

但以这种方式实现的懒汉模式,在单线程下运行是没有问题的,但要运行在多线程下,就会出现实例化多个类对象的情况。也就是线程不安全。

  1. class Singleton_1 {
  2. private static Singleton_1 instance = null;
  3. private Singleton_1() {
  4. }
  5. public static Singleton_1 getInstance() {
  6. if (null == instance) {
  7. instance = new Singleton_1();
  8. }
  9. return instance;
  10. }
  11. }

image.png
虽然是线程不安全的实现方式,但在正常时的性能( 20~36 ms )还是不错的。

Singleton_2

Singleton_2 类:懒汉模式 + synchronized同步锁、线程安全

为了解决 Singleton_1 带来的线程不安全问题,我们需要对 getInstance 方法进行加锁,保证多线程情况下仅创建一个实例。

但同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能。

  1. class Singleton_2 {
  2. private static Singleton_2 instance = null;
  3. private Singleton_2() {
  4. }
  5. public static synchronized Singleton_2 getInstance() {
  6. if (null == instance) {
  7. instance = new Singleton_2();
  8. }
  9. return instance;
  10. }
  11. }

image.png
519~532 ms
虽然是线程安全的,但性能太差了。

Singleton_3

Singleton_3 类: 懒汉模式 + synchronized同步锁 、线程不安全

每次请求获取类对象时,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。基于以上两点,我们可以考虑将同步锁放在 if 条件里面,这样就可以减少同步锁资源竞争。

  1. class Singleton_3 {
  2. private static Singleton_3 instance = null;
  3. private Singleton_3() {
  4. }
  5. public static Singleton_3 getInstance() {
  6. if (null == instance) {
  7. synchronized (Singleton_3.class) {
  8. instance = new Singleton_3();
  9. }
  10. }
  11. return instance;
  12. }
  13. }

image.png
虽然是线程不安全的,但可以看到,在没有获取到非单实例的情况下,性能表现已经比 Singleton_2 要好非常多了。( 30 ms 左右 )

Singleton_4

Singleton_4 类:懒汉模式 + synchronized同步锁 + double-check 、线程可能不安全

Singleton_3 虽然有同步锁,但是进入到判断条件里面的线程依然会依次获取到锁创建对象,然后再释放同步锁。所以我们还需要在同步锁里面再加一次判断。这种方式,通常被称为 Double-Check,它可以大大提高支持多线程的懒汉模式的运行性能。

  1. class Singleton_4 {
  2. private static Singleton_4 instance = null;
  3. private Singleton_4() {
  4. }
  5. public static Singleton_4 getInstance() {
  6. if (null == instance) {
  7. synchronized (Singleton_4.class) {
  8. if (null == instance) {
  9. instance = new Singleton_4();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }

image.png
43~61 ms
可以看到,比之 Singleton_2 (懒汉模式 + synchronized同步锁)的实现方式,性能提升了接近10倍。

Singleton_5

Singleton_5 类:懒汉模式 + synchronized同步锁 + double-check + volatile 线程安全

Singleton_4 之所以说线程可能不安全的原因是:指令重排序。

在 JMM 中,重排序是十分重要的一环,特别是在并发编程中。如果 JVM 可以对它们进行任意排序以提高程序性能,也可能会给并发编程带来一系列的问题。

volatile 在 JDK1.5 之后还有一个作用就是阻止局部重排序的发生,也就是说,volatile 变量的操作指令都不会被重排序。所以使用 volatile 修饰 instance 之后,Double-Check 懒汉单例模式就万无一失了。

  1. class Singleton_5 {
  2. private volatile static Singleton_5 instance = null;
  3. private Singleton_5() {
  4. }
  5. public static Singleton_5 getInstance() {
  6. if (null == instance) {
  7. synchronized (Singleton_5.class) {
  8. if (null == instance) {
  9. instance = new Singleton_5();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }

image.png31~54 ms
性能比之 Singleton_4 并没有下降。

Singleton_6

Singleton_6 类:懒汉模式 + 静态内部类 线程安全

Singleton_5 这种同步锁 +Double-Check 的实现方式相对来说,复杂且加了同步锁,使用内部类实现会更简洁一些。

InnerSingleton 是一个静态内部类,当外部类 Singleton_6 被加载的时候,并不会创建 InnerSingleton 实例对象。只有当调用 getInstance() 方法时,InnerSingleton 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

  1. class Singleton_6 {
  2. private static Singleton_6 instance = null;
  3. private Singleton_6() {
  4. }
  5. // 内部类实现
  6. public static class InnerSingleton {
  7. private static Singleton_6 instance = new Singleton_6();
  8. }
  9. public static Singleton_6 getInstance() {
  10. return InnerSingleton.instance;
  11. }
  12. }

image.png
30~56 ms

Singleton_7

Singleton_7 类:枚举方式实现

最后,我们介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

  1. enum Singleton_7 {
  2. //实例
  3. INSTANCE;
  4. public static Singleton_7 getInstance() {
  5. return INSTANCE;
  6. }
  7. public void doSomething() {
  8. System.out.println("doSomething");
  9. }
  10. }

image.png
24~50 ms
性能与 Singleton ( 饿汉模式 ) 不相上下,

总结

单例的实现方式其实有很多,但总结起来就两种:饿汉模式和懒汉模式,我们可以根据自己的需求来做选择。

如果我们在程序启动后,一定会加载到类,那么用饿汉模式实现的单例简单又实用;如果我们是写一些工具类,则优先考虑使用懒汉模式,因为很多项目可能会引用到 jar 包,但未必会使用到这个工具类,懒汉模式实现的单例可以避免提前被加载到内存中,占用系统资源。