一.事先准备

首先准备一个运行用的代码:

  1. public class Singleton {
  2. public static void main(String[] args) {
  3. Thread[] threads = new Thread[10];
  4. for (int i = 0; i < threads.length; i++) {
  5. threads[i] = new myThread();
  6. }
  7. for (Thread thread : threads) {
  8. thread.start();
  9. }
  10. }
  11. }
  12. class myThread extends Thread {
  13. @Override
  14. public void run() {
  15. //打印实例的hashCode
  16. //运行不同的示例时替换类名即可
  17. System.out.println(Obj.getObj().hashCode());
  18. }
  19. }

以下代码都在此基础上运行。

二.饿汉式

饿汉式是天生线程安全的。

  1. /*
  2. * 饿汉式
  3. * */
  4. class Obj{
  5. //一开始就直接初始化对象
  6. private static Obj obj = new Obj();
  7. //私有有化构造方法避免再new出一个对象
  8. private Obj() {
  9. }
  10. //通过get方法获取实例
  11. public static Obj getObj(){
  12. return obj;
  13. }
  14. }
  15. //输出
  16. 471895473
  17. 471895473
  18. 471895473
  19. 471895473
  20. 471895473
  21. 471895473
  22. 471895473
  23. 471895473
  24. 471895473
  25. 471895473

对应饿汉式,因为饿汉式在类加载时创建实例,而一个类在生命周期中只被加载一次,也就是说,饿汉式在线程访问前就已经创建好了唯一的那个实例,因此无论多少个线程同时访问,最终获取到的都是一个实例。

当然,实际上饿汉式可能导致系统最终创建了太多无用实例,所以懒汉式仍然还是必要的。

三.懒汉式

1.传统懒汉式

传统懒汉式是非线程安全的,示例如下:

  1. /*
  2. * 传统懒汉式
  3. * */
  4. class Obj2 {
  5. private static Obj2 obj;
  6. private Obj2() {
  7. }
  8. //get方法进行同步
  9. public static Obj2 getObj(){
  10. if (obj == null){
  11. obj = new Obj2();
  12. }
  13. return obj;
  14. }
  15. }
  16. //输出
  17. 1217036164 //创建了多个实例
  18. 102629730
  19. 1217036164
  20. 102629730
  21. 102629730
  22. 102629730
  23. 102629730
  24. 102629730

对于传统懒汉式,因为当某个线程创建实例但是还没来得及写入堆内存时,可能已经有多个线程进入了if代码块,因此可能最后会创建多个实例。

2.使用内部类

因为饿汉式是天生线程的,所以也可以通过内部类实现:

  1. /**
  2. * 内部类
  3. */
  4. class Obj5 {
  5. //内部类
  6. private static class InitBean {
  7. //将外部类作为成员变量,饿汉式创建
  8. private static Obj5 obj5 = new Obj5();
  9. }
  10. private Obj5() {
  11. }
  12. //工厂方法,实际上是懒汉式初始化内部类,并且从内部类获取实例
  13. public static Obj5 getObj() {
  14. return InitBean.obj5;
  15. }
  16. }
  17. //输出
  18. 1217036164
  19. 1217036164
  20. 1217036164
  21. 1217036164
  22. 1217036164
  23. 1217036164
  24. 1217036164
  25. 1217036164
  26. 1217036164
  27. 1217036164

对于这种方法的解释是这样的:

当调用getObj()方法时,会触发InitBean类的初始化。

由于Obj5是InitBean的类成员变量,因此在JVM调用InitBean类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。

总结一下,就是jvm会保障多线程情况下类的正确初始化,所以借助这一点,我们创建一个内部类,然后让内部类初始化的时候创建唯一个实例作为成员变量,而通过外部类的工厂方法来触发内部类的初始化并获取实例。

3.使用synchronize同步工厂方法

解决传统懒汉式问题的方法很简单,那就是直接给工厂方法加上synchronize关键字变成同步方法:

  1. ...
  2. //直接对工厂方法进行同步
  3. public static synchronized Obj2 getObj(){
  4. if (obj == null){
  5. obj = new Obj2();
  6. }
  7. return obj;
  8. }
  9. //输出
  10. 1696172314
  11. 1696172314
  12. 1696172314
  13. 1696172314
  14. 1696172314
  15. 1696172314
  16. 1696172314
  17. 1696172314
  18. 1696172314
  19. 1696172314

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,所以我们还得进一步缩小锁范围,因此可以考虑使用同步代码块来实现。

4.双重检查

要说锁粒度最小,那就是只锁实例化的代码,然后只在实例化前进行一次检查:

  1. /*
  2. * 不双重检查
  3. * */
  4. class Obj4 {
  5. private volatile static Obj4 obj4;
  6. private Obj4() {
  7. }
  8. public static Obj4 getObj() {
  9. //检查是否已有实例
  10. if (obj4 == null) {
  11. //没有实例就获取锁进行准备实例化
  12. synchronized (Obj3.class) {
  13. obj4 = new Obj4();
  14. }
  15. }
  16. return obj4;
  17. }
  18. }
  19. //输出
  20. 2140075580 //创建了多个实例
  21. 1285201119
  22. 1217036164
  23. 102629730
  24. 102629730
  25. 1000492474
  26. 1217036164
  27. 569477593
  28. 1217036164
  29. 1217036164

实际上这样跟传统懒汉式并无区别,因为只检查一次的话,同样会面对第一个示例还在创建,结果其他线程直接通过了if判断的情况,所以我们需要再在同步代码块中进行一次检查

  1. ...
  2. //对工厂方法进行双重检查
  3. public static Obj3 getObj() {
  4. //检查是否已有实例
  5. if (obj3 == null) {
  6. //没有实例就获取锁进行准备实例化
  7. synchronized (Obj3.class) {
  8. //再判断是否已经有获取过锁的线程实例化了对象
  9. if (obj3 == null) {
  10. obj3 = new Obj3();
  11. }
  12. }
  13. }
  14. return obj3;
  15. }
  16. //输出
  17. 1696172314
  18. 1696172314
  19. 1696172314
  20. 1696172314
  21. 1696172314
  22. 1696172314
  23. 1696172314
  24. 1696172314
  25. 1696172314
  26. 1696172314

四.双重检查中的volatile关键字

在双重检查中,必须使用volatile关键字修饰引用的单例,目的是jvm在创建实例的时候进行禁止指令重排

要理解指令重排,必须先理解jvm是如何处理new操作的:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 使变量指向对象

但是由于jvm会因为进行指令重排,所一实际上new操作的步骤可能发生变化

  1. 分配对象的内存空间
  2. 使变量指向对象
  3. 初始化对象

这就会导致现有的代码出现这样的问题:

  1. 线程一最先获取锁并执行初始化代码,但是发生了指令重排
  2. jvm执行完1后,先执行了3,但是2还没来得及执行锁就被线程二抢占了
  3. 此时线程二能够获取实例了,通过了代码检查,但是线程二获取的这个实例还没有初始化,是个不完整的实例
  4. 线程一抢占锁,执行构造函数完成变量初始化

显然,如果线程二拿着一个不完整的实例进了业务代码,就会引发各种bug,这种隐患正是由指令重排引起的,所以我们需要使用volatile指令修饰引用的单例

java多线程(三):多线程单例模式,双重检查,volatile关键字的更多相关文章

  1. Java面试官最爱问的volatile关键字

    在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...

  2. 单例模式中的volatile关键字

    在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章   背景:在早期的JVM中,synchr ...

  3. Java面试官最常问的volatile关键字

    在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...

  4. Java多线程干货系列—(四)volatile关键字

    原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...

  5. JAVA并发编程:相关概念及VOLATILE关键字解析

    一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...

  6. java并发:线程同步机制之Volatile关键字&原子操作Atomic

    volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...

  7. Java并发编程学习笔记 深入理解volatile关键字的作用

    引言:以前只是看过介绍volatile的文章,对其的理解也只是停留在理论的层面上,由于最近在项目当中用到了关于并发方面的技术,所以下定决心深入研究一下java并发方面的知识.网上关于volatile的 ...

  8. Java并发编程实战3-可见性与volatile关键字

    1. 缓存一致性问题 在计算机中,每条指令都是在CPU执行的,而CPU又不具备存储数据的功能,因此数据都是存储在主存(即内存)和外存(硬盘)中.但是,主存中数据的存取速度高于外存中数据的存取速度(这也 ...

  9. Java多线程(三) 多线程间的基本通信

    多条线程在操作同一份数据的时候,一般需要程序去控制好变量.在多条线程同时运行的前提下控制变量,涉及到线程通信及变量保护等. 本博文主要总结:①线程是如何通信  ②如何保护线程变量 1.Java里的线程 ...

  10. Java 并发和多线程(三) 多线程的代价 [转]

    原文链接:http://tutorials.jenkov.com/java-concurrency/costs.html 作者:Jakob Jenkov     翻译:古圣昌        校对:欧振 ...

随机推荐

  1. 快速突击 Spring Cloud Gateway

    认识 Spring Cloud Gateway Spring Cloud Gateway 是一款基于 Spring 5,Project Reactor 以及 Spring Boot 2 构建的 API ...

  2. DEX文件解析--7、类及其类数据解析(完结篇)

    一.前言    前置技能链接:       DEX文件解析---1.dex文件头解析       DEX文件解析---2.Dex文件checksum(校验和)解析       DEX文件解析--3.d ...

  3. 3个月不发工资,拖延转正?2天跳槽软件测试成功,9.5k他不香吗!

    今天聊到的小哥哥很悲催又很神奇,身处武汉的他,正好赶上了疫情,不仅长达3个月没有发工资,拖延转正,还要降薪,三重打击,实名悲催. 不破不立,试用期80%再打8折,怎么跳槽工资都得比这高,果然,仅仅两天 ...

  4. awesome : vue-awesome 按需引入

    其实挺简单的,被文档带到沟里去了. main.js: 首先讲一下全部引入,很简单. import 'vue-awesome/icons' import Icon from 'vue-awesome/c ...

  5. JS 原型与原型链终极详解(二)

    四. __proto__ JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__ 的内置属性,用于指向创建它的构造函数的原型对象. 对象 person1 有一个 __pr ...

  6. 写verilog程序需要注意的地方

    1.在always块语句中一定要注意if-else if-else if-else的判断条件的顺序. 2.同一个寄存器信号只能在同一个always or initial 块中进行赋值. 3.在控制一个 ...

  7. 用windbg查看dmp文件,定位bug位置

    windbg + .dmp + .pdb + 源代码,可以看到是哪个代码崩溃的 设置符号文件所在路径 File->Symbol File Path... 在输入框中填入.pdb文件所在的文件夹路 ...

  8. Qt-操作xml文件

    1  简介 参考视频:https://www.bilibili.com/video/BV1XW411x7AB?p=12 xml简介:可扩展标记语言,标准通用标记语言的子集,简称XML.是一种用于标记电 ...

  9. nginx配置多个图片访问路径

    需求:vue项目打包的时候 会将项目中的一些图片打包到/dist/static/images下,但是有时候会有一些很大的图片,需要单独存放至别的文件夹比如/home/di-img下,不能被打倒包内.部 ...

  10. 一个使用android相机的例子,二维码必须用相机

    https://blog.csdn.net/feiduclear_up/article/details/51968975