- 浏览: 2180768 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (682)
- 软件思想 (7)
- Lucene(修真篇) (17)
- Lucene(仙界篇) (20)
- Lucene(神界篇) (11)
- Solr (48)
- Hadoop (77)
- Spark (38)
- Hbase (26)
- Hive (19)
- Pig (25)
- ELK (64)
- Zookeeper (12)
- JAVA (119)
- Linux (59)
- 多线程 (8)
- Nutch (5)
- JAVA EE (21)
- Oracle (7)
- Python (32)
- Xml (5)
- Gson (1)
- Cygwin (1)
- JavaScript (4)
- MySQL (9)
- Lucene/Solr(转) (5)
- 缓存 (2)
- Github/Git (1)
- 开源爬虫 (1)
- Hadoop运维 (7)
- shell命令 (9)
- 生活感悟 (42)
- shell编程 (23)
- Scala (11)
- MongoDB (3)
- docker (2)
- Nodejs (3)
- Neo4j (5)
- storm (3)
- opencv (1)
最新评论
-
qindongliang1922:
粟谷_sugu 写道不太理解“分词字段存储docvalue是没 ...
浅谈Lucene中的DocValues -
粟谷_sugu:
不太理解“分词字段存储docvalue是没有意义的”,这句话, ...
浅谈Lucene中的DocValues -
yin_bp:
高性能elasticsearch ORM开发库使用文档http ...
为什么说Elasticsearch搜索是近实时的? -
hackWang:
请问博主,有用solr做电商的搜索项目?
Solr中Group和Facet的用法 -
章司nana:
遇到的问题同楼上 为什么会返回null
Lucene4.3开发之第八步之渡劫初期(八)
# Java单例模式之双检锁剖析
### 前言
单例模式在Java开发中是非常经典和实用的一种设计模式,在JDK的内部包的好多api都采用了单例模式,如我们熟悉的Runtime类,单例模式总的来说有两种创建方式,一种是延迟加载的模式,一种是非延迟加载的模式,今天我们来学习一下基于双检锁延迟加载的单例模式。
### 什么是单例模式
顾名思义,单例模式指的是在整个程序运行期间,我们只能初始化某个类一次,然后一直使用这个实例,尤其是在多线程的环境下,也要保证如此。
### 基于双检锁的单例模式
在介绍基于双检锁的单例模式下,我们先思考下在使用延迟加载的情况下,如何实现一个单例模式,可能有一些比较年轻的小伙伴,不假思索的就写下了下面的一段代码:
上面的代码在单线程的环境下是没有问题的,但是在多线程的环境下是不能保证只创建一个实例的,
然后小伙伴想了下,这还不简单,加个同步关键字就可以了:
嗯,这下看起来没问题,但唯一的不足就是,这段代码虽然可以保证只创建一个单例,但其性能不高,因为每次访问这个方法的时候都需要执行同步操作,那么有没有方法可以避免这一个缺点呢?这个时候我们就可以用双检锁的模式了:
想要彻底理解双检锁模式的原理,首先要明白在Java里面一个线程对共享变量的修改,对于另外一个线程是不可预知的,也就是说它可能看不到变化,也有可能会看到,虽然在大多数时候是看不到的,但这不能证明它总是会被看到,除非正确的使用同步,否则是没法掌控的。
上面的基础认知非常重要,我原来就理解错误了,因为我通过代码检测出来,一个线程的修改对于另外一个线程是不可见的,所以就一直认为总是不可见的。但其实这是不正确的认识,因为编写多线程代码可能是容易的,但测试多线程程序是非常复杂的,或者说在一些情况下,没有人知道应该怎么测和怎么复现多线程bug,这也是多线程程序很难调试的的原因。
关于双检锁里面为什么必须要加volatile关键字,主要用来避免重排序问题导致其他的线程看到了一个已经分配内存和地址但没有初始化的对象,也就是说这个对象还不是处于可用状态,就被其他线程引用了。
下面的代码在多线程环境下不是原子执行的。
正常的底层执行顺序会转变成三步:
上面的三步,无论在A线程当前执行到那一步骤,对B线程来说可能看到A的状态只能是两种1,2看到的都是null,3看到的非null,这是没问题的。
但是如果线程A在重排序的情况下,上面的执行顺序会变成1,3,2。现在假设A线程按1,3,2三个步骤顺序执行,当执行到第二步的时候。B线程开始调用这个方法,那么在第一个null的检查的时候,就有可能看到这个实例不是null,然后直接返回这个实例开始使用,但其实是有问题的,因为对象还没有初始化,状态还处于不可用的状态,故而会导致异常发生。
要解决这个问题,可以通过volatile关键词来避免指令重排序,这里相比可见性问题主要是为了避免重排序问题。如果使用了volatile修饰成员变量,那么在变量赋值之后,会有一个内存屏障。也就说只有执行完1,2,3步操作后,读取操作才能看到,读操作不会被重排序到写操作之前。这样以来就解决了对象状态不完整的问题。
那么volatile到底如何保证可见性和禁止指令重排序的
在《深入理解Java虚拟机》一书中有描述:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
从上面可以看到volatile不保证原子性,保证可见性和部分有序性,这一点需要谨记。
此外这里需要注意的是在JDK5之前,就算加了volatile关键字也依然有问题,原因是之前的JMM模型是有缺陷,volatile变量前后的代码仍然可以出现重排序问题,这个问题在JDK5之后才得到解决,所以现在才可以这么使用。
正是因为双检锁的单例模式涉及的底层知识比较多,所以在面试中也是经常被问的一个话题。
### 其他的单例实现
前面说到过,单例模式从创建方式来说有懒汉(延迟加载)和非懒汉就是饿汉的单例模式。关于懒汉模式的除了双检锁模式,还有通过静态内部类实现的如下:
静态内部类是由JVM内部的锁机制来保证不会创建多个实例,非常巧妙的避开了多线程问题。
关于饿汉的单例模式形象点说,就是我不管你到底用不用得到都提前给你准备好。相比懒汉需要考虑各种线程问题,饿汉就比较简单了,第一种,非常简单:
第二种,基于枚举方式:
基于枚举的方式非常简洁,而且非常安全由jvm内部保证,自带私有的构造方法并且序列化和反射都不会破坏单例的安全性,据说是JDK5之后最好的单例创建方式,这个具体还是分应用场景。
### 总结
本篇文章重点介绍了在Java里面双检锁模式如何实现懒汉的单例模式,并分析其背后的原理和JMM的相关的一些知识,此外还介绍了其他的一些常用的单例模式供大家参考,感兴趣的小伙伴可以自己动手尝试一下。最后文中所有的代码已经上传到我的github,需要的朋友可以去fork运行。
https://github.com/qindongliang/Java-Note
### 前言
单例模式在Java开发中是非常经典和实用的一种设计模式,在JDK的内部包的好多api都采用了单例模式,如我们熟悉的Runtime类,单例模式总的来说有两种创建方式,一种是延迟加载的模式,一种是非延迟加载的模式,今天我们来学习一下基于双检锁延迟加载的单例模式。
### 什么是单例模式
顾名思义,单例模式指的是在整个程序运行期间,我们只能初始化某个类一次,然后一直使用这个实例,尤其是在多线程的环境下,也要保证如此。
### 基于双检锁的单例模式
在介绍基于双检锁的单例模式下,我们先思考下在使用延迟加载的情况下,如何实现一个单例模式,可能有一些比较年轻的小伙伴,不假思索的就写下了下面的一段代码:
``` private static DoubleCheckSingleton instance; //私有的构造方法 private DoubleCheckSingleton() {} public static DoubleCheckSingleton getErrorInstance(){ if (instance==null){ instance=new DoubleCheckSingleton(); } return instance; } ```
上面的代码在单线程的环境下是没有问题的,但是在多线程的环境下是不能保证只创建一个实例的,
然后小伙伴想了下,这还不简单,加个同步关键字就可以了:
``` private static DoubleCheckSingleton instance; //私有的构造方法 private DoubleCheckSingleton() {} public synchronized static DoubleCheckSingleton getErrorInstance(){ if (instance==null){ instance=new DoubleCheckSingleton(); } return instance; } ```
嗯,这下看起来没问题,但唯一的不足就是,这段代码虽然可以保证只创建一个单例,但其性能不高,因为每次访问这个方法的时候都需要执行同步操作,那么有没有方法可以避免这一个缺点呢?这个时候我们就可以用双检锁的模式了:
``` private volatile static DoubleCheckSingleton instance; //私有的构造方法 private DoubleCheckSingleton() {} public static DoubleCheckSingleton getInstance(){ if(instance==null){ //第一层检查 synchronized (DoubleCheckSingleton.class){ if(instance==null){ //第二层检查 instance=new DoubleCheckSingleton(); } } } return instance; } ```
想要彻底理解双检锁模式的原理,首先要明白在Java里面一个线程对共享变量的修改,对于另外一个线程是不可预知的,也就是说它可能看不到变化,也有可能会看到,虽然在大多数时候是看不到的,但这不能证明它总是会被看到,除非正确的使用同步,否则是没法掌控的。
上面的基础认知非常重要,我原来就理解错误了,因为我通过代码检测出来,一个线程的修改对于另外一个线程是不可见的,所以就一直认为总是不可见的。但其实这是不正确的认识,因为编写多线程代码可能是容易的,但测试多线程程序是非常复杂的,或者说在一些情况下,没有人知道应该怎么测和怎么复现多线程bug,这也是多线程程序很难调试的的原因。
关于双检锁里面为什么必须要加volatile关键字,主要用来避免重排序问题导致其他的线程看到了一个已经分配内存和地址但没有初始化的对象,也就是说这个对象还不是处于可用状态,就被其他线程引用了。
下面的代码在多线程环境下不是原子执行的。
``` instance=new DoubleCheckSingleton(); ```
正常的底层执行顺序会转变成三步:
```java (1) 给DoubleCheckSingleton类的实例instance分配内存 (2) 调用实例instance的构造函数来初始化成员变量 (3) 将instance指向分配的内存地址 ```
上面的三步,无论在A线程当前执行到那一步骤,对B线程来说可能看到A的状态只能是两种1,2看到的都是null,3看到的非null,这是没问题的。
但是如果线程A在重排序的情况下,上面的执行顺序会变成1,3,2。现在假设A线程按1,3,2三个步骤顺序执行,当执行到第二步的时候。B线程开始调用这个方法,那么在第一个null的检查的时候,就有可能看到这个实例不是null,然后直接返回这个实例开始使用,但其实是有问题的,因为对象还没有初始化,状态还处于不可用的状态,故而会导致异常发生。
要解决这个问题,可以通过volatile关键词来避免指令重排序,这里相比可见性问题主要是为了避免重排序问题。如果使用了volatile修饰成员变量,那么在变量赋值之后,会有一个内存屏障。也就说只有执行完1,2,3步操作后,读取操作才能看到,读操作不会被重排序到写操作之前。这样以来就解决了对象状态不完整的问题。
那么volatile到底如何保证可见性和禁止指令重排序的
在《深入理解Java虚拟机》一书中有描述:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
``` 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时 ,在它前面的操作已经全部完成; 2)它会强制将对缓存的修改操作立即写入主存; 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 ```
从上面可以看到volatile不保证原子性,保证可见性和部分有序性,这一点需要谨记。
此外这里需要注意的是在JDK5之前,就算加了volatile关键字也依然有问题,原因是之前的JMM模型是有缺陷,volatile变量前后的代码仍然可以出现重排序问题,这个问题在JDK5之后才得到解决,所以现在才可以这么使用。
正是因为双检锁的单例模式涉及的底层知识比较多,所以在面试中也是经常被问的一个话题。
### 其他的单例实现
前面说到过,单例模式从创建方式来说有懒汉(延迟加载)和非懒汉就是饿汉的单例模式。关于懒汉模式的除了双检锁模式,还有通过静态内部类实现的如下:
``` public class HolderFactory { public static Singleton get() { return Holder.instance; } private static class Holder { public static final Singleton instance = new Singleton(); } } ```
静态内部类是由JVM内部的锁机制来保证不会创建多个实例,非常巧妙的避开了多线程问题。
关于饿汉的单例模式形象点说,就是我不管你到底用不用得到都提前给你准备好。相比懒汉需要考虑各种线程问题,饿汉就比较简单了,第一种,非常简单:
``` private static SimpleSingleton ourInstance = new SimpleSingleton(); public static SimpleSingleton getInstance() { return ourInstance; } private SimpleSingleton() { } ```
第二种,基于枚举方式:
``` public enum EnumSingleton { SINGLETON; } ```
基于枚举的方式非常简洁,而且非常安全由jvm内部保证,自带私有的构造方法并且序列化和反射都不会破坏单例的安全性,据说是JDK5之后最好的单例创建方式,这个具体还是分应用场景。
### 总结
本篇文章重点介绍了在Java里面双检锁模式如何实现懒汉的单例模式,并分析其背后的原理和JMM的相关的一些知识,此外还介绍了其他的一些常用的单例模式供大家参考,感兴趣的小伙伴可以自己动手尝试一下。最后文中所有的代码已经上传到我的github,需要的朋友可以去fork运行。
https://github.com/qindongliang/Java-Note
发表评论
-
记一次log4j不打印日志的踩坑记
2019-09-22 01:58 1556### 起因 前几天一个跑有java应用的生产集群(200多 ... -
在Java里面如何解决进退两难的jar包冲突问题?
2019-07-23 19:10 1227如上图所示: es api组件依赖guava18.0 ... -
如何轻松理解二叉树的深度遍历策略
2019-07-03 23:33 1119我们知道普通的线性数据结构如链表,数组等,遍历方式单一 ... -
为什么单线程Redis性能也很出色
2019-01-21 18:02 2201高性能的服务器,不一 ... -
如何将编程语言里面的字符串转成数字?
2019-01-11 23:23 2085将字符串转成数字在很 ... -
为什么Java里面String类是不可变的
2019-01-06 18:36 1663在Java里面String类型是不可变对象,这一点毫无疑问,那 ... -
关于Java里面volatile关键字的重排序
2019-01-04 18:49 1060Java里面volatile关键字主 ... -
多个线程如何轮流打印ABC特定的次数?
2018-12-11 20:42 6030之前的一篇文章,我给 ... -
聊聊Java里面的引用传递
2018-11-16 21:21 987长久以来,在Java语言里面一直有一个争论,就是Java语言到 ... -
理解计数排序算法的原理和实现
2018-10-11 10:03 2087计数排序(Counting sort) ... -
理解Java7和8里面HashMap+ConcurrentHashMap的扩容策略
2018-09-06 11:31 3385### 前言 理解HashMap和Con ... -
关于Java里面多线程同步的一些知识
2018-07-18 09:45 1102# 关于Java里面多线程同步的一些知识 对于任何Java开 ... -
关于Java里面多线程同步的一些知识
2018-07-08 12:23 1119# 关于Java里面多线程同步的一些知识 对于任何Java开 ... -
重新认识同步与异步,阻塞和非阻塞的概念
2018-07-06 14:30 1469# 重新认识同步与异步 ... -
线程的基本知识总结
2018-06-27 16:27 1057### (一)创建线程的方式 (1)实现Runnable接口 ... -
Java里面volatile关键字修饰引用变量的陷阱
2018-06-25 11:42 1384# Java里面volatile关键字修饰引用变量的陷阱 如 ... -
关于Java里面的字符串拼接,你了解多少?
2018-06-25 11:28 1364# 关于Java里面的字符串 ... -
深入理解Java内存模型的语义
2018-06-25 11:39 735### 前言 Java内存模型( ... -
如何证明Java多线程中的成员变量数据是互不可见的
2018-06-21 10:09 1496前面的几篇文章主要介绍了Java的内存模型,进程和线程的定义, ... -
给Java字节码加上”翅膀“的JIT编译器
2018-06-20 10:12 1033# 给Java字节码加上”翅 ...
相关推荐
### Java 单例模式详解 #### 一、什么是单例模式? 单例模式是一种常用的软件设计模式,在这种模式中,一个类只能拥有一个实例,并且该类必须自行创建并提供这个实例。通常,单例模式用于确保某个类在整个应用程序...
Java设计模式是面向对象编程中的重要概念,它们是软件开发中经过验证的、解决常见问题的最佳实践。...观看这些视频,可以更深入地理解并掌握Java中的单例模式,从而在实际开发中灵活运用,提升代码质量。
在Java中,可以使用双检锁/双重检查锁定(Double-Checked Locking)或者静态内部类的方式来实现单例,以保证线程安全。 工厂模式则是一种创建型模式,它提供了一种创建对象的最佳方式,避免了在客户端代码中直接new...
3. **模式间的相互关系**:了解不同设计模式之间的关联和区别,比如装饰器和代理模式的区别,或者单例模式与静态内部类的实现差异。 4. **模式的优缺点**:评估每种模式的适用性和潜在问题,如过度设计或性能影响。 ...
本资料“java设计模式学习”包含了对设计模式的深入理解和实际应用,通过简单实用的例子,帮助开发者掌握如何在Java项目中运用设计模式。 首先,我们要介绍的是工厂模式。工厂模式是一种创建型设计模式,它提供了一...
2. **创建型模式**:包括单例模式、工厂模式(简单工厂、工厂方法、抽象工厂)、建造者模式、原型模式等,它们关注于如何创建对象,减少类之间的耦合。 3. **结构型模式**:如适配器模式、装饰器模式、代理模式、...
在深入探讨《用Java模式思考》(Thinking in Patterns with Java)这一主题之前,我们首先需要了解设计模式的基本概念以及它们如何被应用于Java编程语言中。本书不仅为读者提供了丰富的理论知识,还通过实际示例帮助...
另外,工厂模式常用于创建数据库连接池,如C3P0或DBCP,而单例模式常用于配置管理类,确保在整个应用中只有一个实例存在。 书中还会讨论到面向对象设计的原则,如单一职责原则(SRP)、开闭原则(OCP)、里氏替换...
《java设计模式(第2版)》通过一个完整的java项目对经典著作design patterns一书介绍的23种设计模式进行了深入分析与讲解,实践性强,却又不失对模式本质的探讨。本书创造性地将这些模式分为5大类别,以充分展现各个...
创建型模式关注对象的创建,如单例模式、工厂模式和建造者模式等,它们提供了不同方式来创建对象,使得代码更加灵活,易于维护。结构型模式处理对象组合和继承关系,如适配器模式、装饰器模式和代理模式等,这些模式...
在设计模式的学习中,UML类图用于描绘各个设计模式的结构和交互,如抽象工厂模式、单例模式、工厂方法模式等,通过图形化的方式使复杂的设计模式变得易于理解。 2. **Java源码**:每个设计模式都配有Java实现的源...
1. "Java与模式":这部分可能是一个文档或教程,详细解释了Java语言中各种设计模式的实现和应用,包括单例模式、工厂模式、观察者模式、装饰器模式等23种经典的GOF设计模式。 2. "《设计模式可复用面向对象软件的...
例如,单例模式保证一个类只有一个实例,而工厂方法则提供了一种创建对象的抽象方式,降低了耦合度。 第二版的《Java设计模式》中文版提供了丰富的例子和深入的解析,涵盖了23个经典的设计模式,不仅讲解了每个模式...
创建型模式关注对象的创建,如单例模式(Singleton)、工厂方法模式(Factory Method)和抽象工厂模式(Abstract Factory)。结构型模式涉及如何组合类和对象,比如适配器模式(Adapter)、装饰器模式(Decorator)...
1. **创建型模式**:这类模式主要关注对象的创建过程,包括单例模式(Singleton)、工厂模式(Factory Method)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。例如,单例模式...
2. **子系统(SubSystem)角色**:这些是实际执行特定任务的类或对象,它们可以是其他设计模式的实例,如工厂模式、单例模式等。 3. **客户端(Client)角色**:通过调用外观角色的方法来与子系统进行交互,无需了解...
在Java中,可以通过静态内部类、枚举或者双检锁(Double-Check Locking)等方式实现。这种模式常用于配置管理、日志服务等场景,避免过多实例化导致资源浪费。 装饰器模式是一种结构型模式,可以在运行时动态地给...
1. 创建型模式:这类模式关注对象的创建过程,如单例模式(Singleton)、工厂模式(Factory)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。这些模式可以帮助我们控制对象的...
2. **Think In Java.chm**:这是经典的《深入思考Java》电子版,作者Bruce Eckel深入浅出地讲解了Java语言的核心概念,包括面向对象编程、集合框架、多线程、异常处理等内容,是Java初学者必读的书籍之一。...