Java单例模式中的线程安全问题

 更新时间:2022年06月20日 10:09:52   作者:XH学Java  
本文主要介绍了Java单例模式中的线程安全问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一. 使用多线程需要考虑的因素

提高效率:使用多线程就是为了充分利用CPU资源,提高任务的效率
线程安全:使用多线程最基本的就是保障线程安全问题

所以我们在设计多线程代码的时候就必须在满足线程安全的前提下尽可能的提高任务执行的效
故:
加锁细粒度化:加锁的代码少一点,让其他代码可以并发并行的执行

🍬考虑线程安全:

没有操作共享变量的代码没有安全问题
对共享变量的读,使用volatile修饰变量即可
对共享变量的写,使用synchronized加锁

🍊二. 单例模式

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例
例如:DataSource(数据连接池),一个数据库只需要一个连接池对象

单例模式分为饿汉模式和懒汉模式

🌴1. 饿汉模式

饿汉模式是在类加载的时候就创建实例
这种方式是满足线程安全的(JVM内部使用了加锁,即多个线程调用静态方法,只有一个线程竞争到锁并且完成创建,只执行一次)

实现代码:

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){
 
    }
    public static Singleton getInstance(){
        return instance;
    }
}

🌾2. 懒汉模式

懒汉模式是在类加载的时候不创建实例,第一次使用的时候才创建

实现代码:

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

观察上述代码,在单线程下不存在线程安全问题,但是在多线程环境下存在安全问题吗? 

分析:
🍃当实例没有被创建的时候,如果有多个线程都调用getInstance方法,就可能创建多个实例,就存在线程安全问题 
🍃但是实例一旦创建好,后面线程调用getInstance方法就不会出现线程安全问题

结果:线程安全问题出现在首次创建实例的时候

🌵3. 懒汉模式(使用synchronized改进)

我们使用sychronized修饰,👁‍🗨️代码如下:

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

这样实现线程安全存在什么问题呢?

解析:
我们对方法使用synchronized修饰,也就是每次调用该方法的时候都会竞争锁,但是创建实例只需要创建一次,也就是创建实例后,再调用该方法还需要竞争锁释放锁

结果:虽然满足线程安全,但是效率低

4. 懒汉模式(使用双重校验锁改进)

在上述代码的基础上进行改动:

使用双重if判定,降低竞争锁频率
使用volatile修饰instance 

实现代码:

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){
 
    }
    public static synchronized Singleton getInstance(){
        if(instance == null){ //外层的if判断:如果实例被创建直接return,不让线程再继续竞争锁
            //在没有创建实例时,多个线程已经进入if判断了
            //一个线程竞争到锁,其他线程阻塞等待
            synchronized (Singleton.class) {
                //内层的if判断,目的是让竞争失败的锁如果再次竞争成功的话判断实例是否被创建,创建释放锁return,没有则创建
                if(instance == null){ 
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

🍬对双重if的解析:

🍂外层的if判断:实例只是被创建一次,当实例已经被创建好了就不要后续操作,直接return返回
🍂内层的if判断:实例未被创建时,多个线程同时竞争锁,只有一个线程竞争成功并创建实例,其他竞争失败的线程就会阻塞等待,当第一线程释放锁后,这些竞争失败的线程就会继续竞争,但是实例已经创建好了,所以需要再次进行if判断 

画图分析,如下所示:

三. volatile的原理 

volatile保证了可见性,有序性,在Java层面看,volatile是无锁操作,多个线程对volatile修饰的变量进行读可以并发并行执行,和无锁执行效率差不多

volatile修饰的变量中,CPU使用了缓存一致性协议来保证读取的都是最新的主存数据 

缓存一致性:如果有别的线程修改了volatile修饰的变量,就会把CPU缓存中的变量置为无效,要操作这个变量就要从主存中重新读取

🍏四. volatile的扩展问题(了解)

🍬如果说volatile不保证有序性,双重校验锁的写法是否有问题?

关于new对象按顺序分为3条指令:

🍁(1) 分配对象的内存空间
🍁(2) 实例化对象
🍁(3) 赋值给变量

正常的执行顺序为(1)(2)(3),JVM可能会优化进行重排序后的顺序为(1)(3)(2)

这个重排序的结果可能导致分配内存空间后,对象还没有实例化完成,就完成了赋值
在这个错误的赋值后,instance==null不成立,线程就会拿着未完成实例化的instance,使用它的属性和方法就会出错

使用volatile保证有序性后:

线程在new对象时不管(1)(2)(3)是什么顺序,后续线程拿到的instance是已经实例化完成的
CPU里边,基于volatile变量操作是有CPU级别的加锁机制(它保证(1)(2)(3)全部执行完,写回主存,再执行其他线程对该变量的操作)

 到此这篇关于Java单例模式中的线程安全问题的文章就介绍到这了,更多相关Java单例模式线程安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • spring web.xml指定配置文件过程解析

    spring web.xml指定配置文件过程解析

    这篇文章主要介绍了spring web.xml指定配置文件过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • java中的常用集合类整理

    java中的常用集合类整理

    本篇文章给大家带来的内容是关于java中List集合及其实现类的方法介绍(附代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。下面我们就来学习一下吧,希望能给你带来帮助
    2021-06-06
  • 海量数据去重排序bitmap(位图法)在java中实现的两种方法

    海量数据去重排序bitmap(位图法)在java中实现的两种方法

    今天小编就为大家分享一篇关于海量数据去重排序bitmap(位图法)在java中实现的两种方法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • intelij idea 2023创建java web项目的完整步骤

    intelij idea 2023创建java web项目的完整步骤

    这篇文章主要给大家介绍了关于intelij idea 2023创建java web项目的完整步骤,该教学主要针对各位刚刚接触javaweb开发的小伙伴,各位学习java的朋友也难免会经历这个阶段,需要的朋友可以参考下
    2023-10-10
  • 很多人竟然不知道Java线程池的创建方式有7种

    很多人竟然不知道Java线程池的创建方式有7种

    本文主要介绍了很多人竟然不知道Java线程池的创建方式有7种,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • java中计算字符串长度的方法及u4E00与u9FBB的认识

    java中计算字符串长度的方法及u4E00与u9FBB的认识

    字符串采用unicode编码的方式时,计算字符串长度的方法找出UNICODE编码中的汉字的代表的范围“\u4E00” 到“\u9FBB”之间感兴趣的朋友可以参考本文,或许对你有所帮助
    2013-01-01
  • Java基础之Comparable与Comparator概述

    Java基础之Comparable与Comparator概述

    这篇文章主要介绍了Java基础之Comparable与Comparator详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • 全面解析Java中的注解与注释

    全面解析Java中的注解与注释

    这篇文章主要介绍了Java中的注解与注释,简单来说注解以@符号开头而注释被包含在/***/符号中,各自具体的作用则来看本文详解,需要的朋友可以参考下
    2016-05-05
  • Java应用服务器之tomcat会话复制集群配置的示例详解

    Java应用服务器之tomcat会话复制集群配置的示例详解

    这篇文章主要介绍了Java应用服务器之tomcat会话复制集群配置的相关知识,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • Java使用JDBC向MySQL数据库批次插入10W条数据(测试效率)

    Java使用JDBC向MySQL数据库批次插入10W条数据(测试效率)

    使用JDBC连接MySQL数据库进行数据插入的时候,特别是大批量数据连续插入(100000),如何提高效率呢?今天小编通过本教程给大家介绍下
    2016-12-12

最新评论