java多线程(三):多线程单例模式,双重检查,volatile关键字
一.事先准备
首先准备一个运行用的代码:
public class Singleton {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new myThread();
}
for (Thread thread : threads) {
thread.start();
}
}
}
class myThread extends Thread {
@Override
public void run() {
//打印实例的hashCode
//运行不同的示例时替换类名即可
System.out.println(Obj.getObj().hashCode());
}
}
以下代码都在此基础上运行。
二.饿汉式
饿汉式是天生线程安全的。
/*
* 饿汉式
* */
class Obj{
//一开始就直接初始化对象
private static Obj obj = new Obj();
//私有有化构造方法避免再new出一个对象
private Obj() {
}
//通过get方法获取实例
public static Obj getObj(){
return obj;
}
}
//输出
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473
对应饿汉式,因为饿汉式在类加载时创建实例,而一个类在生命周期中只被加载一次,也就是说,饿汉式在线程访问前就已经创建好了唯一的那个实例,因此无论多少个线程同时访问,最终获取到的都是一个实例。
当然,实际上饿汉式可能导致系统最终创建了太多无用实例,所以懒汉式仍然还是必要的。
三.懒汉式
1.传统懒汉式
传统懒汉式是非线程安全的,示例如下:
/*
* 传统懒汉式
* */
class Obj2 {
private static Obj2 obj;
private Obj2() {
}
//get方法进行同步
public static Obj2 getObj(){
if (obj == null){
obj = new Obj2();
}
return obj;
}
}
//输出
1217036164 //创建了多个实例
102629730
1217036164
102629730
102629730
102629730
102629730
102629730
对于传统懒汉式,因为当某个线程创建实例但是还没来得及写入堆内存时,可能已经有多个线程进入了if代码块,因此可能最后会创建多个实例。
2.使用内部类
因为饿汉式是天生线程的,所以也可以通过内部类实现:
/**
* 内部类
*/
class Obj5 {
//内部类
private static class InitBean {
//将外部类作为成员变量,饿汉式创建
private static Obj5 obj5 = new Obj5();
}
private Obj5() {
}
//工厂方法,实际上是懒汉式初始化内部类,并且从内部类获取实例
public static Obj5 getObj() {
return InitBean.obj5;
}
}
//输出
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
对于这种方法的解释是这样的:
当调用getObj()方法时,会触发InitBean类的初始化。
由于Obj5是InitBean的类成员变量,因此在JVM调用InitBean类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。
总结一下,就是jvm会保障多线程情况下类的正确初始化,所以借助这一点,我们创建一个内部类,然后让内部类初始化的时候创建唯一个实例作为成员变量,而通过外部类的工厂方法来触发内部类的初始化并获取实例。
3.使用synchronize同步工厂方法
解决传统懒汉式问题的方法很简单,那就是直接给工厂方法加上synchronize关键字变成同步方法:
...
//直接对工厂方法进行同步
public static synchronized Obj2 getObj(){
if (obj == null){
obj = new Obj2();
}
return obj;
}
//输出
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,所以我们还得进一步缩小锁范围,因此可以考虑使用同步代码块来实现。
4.双重检查
要说锁粒度最小,那就是只锁实例化的代码,然后只在实例化前进行一次检查:
/*
* 不双重检查
* */
class Obj4 {
private volatile static Obj4 obj4;
private Obj4() {
}
public static Obj4 getObj() {
//检查是否已有实例
if (obj4 == null) {
//没有实例就获取锁进行准备实例化
synchronized (Obj3.class) {
obj4 = new Obj4();
}
}
return obj4;
}
}
//输出
2140075580 //创建了多个实例
1285201119
1217036164
102629730
102629730
1000492474
1217036164
569477593
1217036164
1217036164
实际上这样跟传统懒汉式并无区别,因为只检查一次的话,同样会面对第一个示例还在创建,结果其他线程直接通过了if判断的情况,所以我们需要再在同步代码块中进行一次检查
...
//对工厂方法进行双重检查
public static Obj3 getObj() {
//检查是否已有实例
if (obj3 == null) {
//没有实例就获取锁进行准备实例化
synchronized (Obj3.class) {
//再判断是否已经有获取过锁的线程实例化了对象
if (obj3 == null) {
obj3 = new Obj3();
}
}
}
return obj3;
}
//输出
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
四.双重检查中的volatile关键字
在双重检查中,必须使用volatile关键字修饰引用的单例,目的是jvm在创建实例的时候进行禁止指令重排。
要理解指令重排,必须先理解jvm是如何处理new操作的:
- 分配对象的内存空间
- 初始化对象
- 使变量指向对象
但是由于jvm会因为进行指令重排,所一实际上new操作的步骤可能发生变化:
- 分配对象的内存空间
- 使变量指向对象
- 初始化对象
这就会导致现有的代码出现这样的问题:
- 线程一最先获取锁并执行初始化代码,但是发生了指令重排
- jvm执行完1后,先执行了3,但是2还没来得及执行锁就被线程二抢占了
- 此时线程二能够获取实例了,通过了代码检查,但是线程二获取的这个实例还没有初始化,是个不完整的实例
- 线程一抢占锁,执行构造函数完成变量初始化
显然,如果线程二拿着一个不完整的实例进了业务代码,就会引发各种bug,这种隐患正是由指令重排引起的,所以我们需要使用volatile指令修饰引用的单例
java多线程(三):多线程单例模式,双重检查,volatile关键字的更多相关文章
- Java面试官最爱问的volatile关键字
在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...
- 单例模式中的volatile关键字
在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章 背景:在早期的JVM中,synchr ...
- Java面试官最常问的volatile关键字
在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...
- Java多线程干货系列—(四)volatile关键字
原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...
- JAVA并发编程:相关概念及VOLATILE关键字解析
一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...
- java并发:线程同步机制之Volatile关键字&原子操作Atomic
volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...
- Java并发编程学习笔记 深入理解volatile关键字的作用
引言:以前只是看过介绍volatile的文章,对其的理解也只是停留在理论的层面上,由于最近在项目当中用到了关于并发方面的技术,所以下定决心深入研究一下java并发方面的知识.网上关于volatile的 ...
- Java并发编程实战3-可见性与volatile关键字
1. 缓存一致性问题 在计算机中,每条指令都是在CPU执行的,而CPU又不具备存储数据的功能,因此数据都是存储在主存(即内存)和外存(硬盘)中.但是,主存中数据的存取速度高于外存中数据的存取速度(这也 ...
- Java多线程(三) 多线程间的基本通信
多条线程在操作同一份数据的时候,一般需要程序去控制好变量.在多条线程同时运行的前提下控制变量,涉及到线程通信及变量保护等. 本博文主要总结:①线程是如何通信 ②如何保护线程变量 1.Java里的线程 ...
- Java 并发和多线程(三) 多线程的代价 [转]
原文链接:http://tutorials.jenkov.com/java-concurrency/costs.html 作者:Jakob Jenkov 翻译:古圣昌 校对:欧振 ...
随机推荐
- bzoj4300绝世好题
bzoj4300绝世好题 题意: 给定一个长度为n的数列ai,求ai的子序列bi的最长长度,满足bi&bi-1!=0.n≤100000,ai≤10^9. 题解: 用f[i]表示当前二进制i为1 ...
- redis入门指南(五)—— 复制与哨兵
写在前面 学习<redis入门指南>笔记,结合实践,只记录重要,明确,属于新知的相关内容. 一.复制 1.在复制中,数据库分为两类,一类主数据库,一类从数据库,主库用来读写,从库用来读,主 ...
- 小谢第36问:elemet - table表格修改后表格行高亮显示且定位到当前行当前页
第一次做这个需求得时候很乱,总是在表格页和修改页徘徊,总觉得什么都会,但是就是做不出自己想要得效果 其实如果先把思路搞清楚,这个问题得知识点却是不多,以下是我对表格高亮显示得思路: 首先,我会从已知得 ...
- Kite: 一个分布式微服务框架(翻译)
原文链接:https://blog.gopheracademy.com/birthday-bash-2014/kite-microservice-library/ 此为中文翻译 用GO语言来编写web ...
- 性能测试必备知识(2)- 查看 Linux 的 CPU 相关信息
做性能测试的必备知识系列,可以看下面链接的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html 查看系统 CPU 信息 cat /proc/ ...
- Python Ethical Hacking - WEB PENETRATION TESTING(2)
CRAWING DIRECTORIES Directories/folders inside the web root. Can contain files or other directories ...
- springboot 跨域设置
/** * Configuration cors */ @Configuration public class MyConfiguration { @Bean public FilterRegistr ...
- 微信小程序 springboot nginx 做图片存储 上传 浏览
微信小程序前端-springboot后端-nginx图片存储 前言 本人小白一名,这是第一次学习微信小程序,特此做个记录. 首先准备nginx做图片存储 选择一个地址存放图片 #我的地址 [root@ ...
- 使用ASP.NET实现定时计划任务,不依靠windows服务
我们怎样才能在服务器上使用asp.net定时执行任务而不需要安装windows service?我们经常需要运行一些维护性的任务或者像发送提醒邮件给用户这样的定时任务.这些仅仅通过使用Windows ...
- Spring Security 实战干货:从零手写一个验证码登录
1. 前言 前面关于Spring Security写了两篇文章,一篇是介绍UsernamePasswordAuthenticationFilter,另一篇是介绍 AuthenticationManag ...