JAVA设计模式详解(四)----------单例模式
上一章我们学习了装饰者模式,这章LZ带给大家的是单例模式。
首先单例模式是用来干嘛的?它是用来实例化一个独一无二的对象!那这有什么用处?有一些对象我们只需要一个,比如缓存,线程池等。而事实上,这类对象只能有一个示例,如果制造多个示例,就会导致许多问题产生,比如程序的行为异常,资源使用过量。而单例模式可以帮助我们确保只有一个实例会被创建。首先我们来看一段代码:
public class MyClass {
private static MyClass myClass;
private MyClass(){
}
public static MyClass getInstance(){
if(myClass ==null){
myClass = new MyClass();
}
return myClass;
}
}
1.首先我们创建一个静态实例,而带有static关键字的属性在每一个类中都是唯一的。
2.接着我们将构造方法私有化,从而限制调用者随意创造实例,这也是保证单例的最重要的一步。
3.当然,我们必须要给一个可供调用方使用的获取实例的静态方法,这里必须是静态方法,为什么呢?请注意,如果我们给的是非静态的,那么调用方必须拥有实例才能调用这个方法,但是既然没有调用这个方法,调用方又哪里来的实例呢?这不是自相矛盾吗
4.我们加一个判断,当只有持有的静态实例为null时才调用构造方法创造一个实例并把它赋予myClass静态变量中,注意,如果我们不需要这个实例,它就永远不会产生,这就是“延迟实例化”。
由此我们可以看出来,单例模式确保一个类只有一个实例,并提供一个全局访问点。

是不是很简单?事实上单例模式确实特别简单,不过LZ还有些内容没有说完。
如果各位去公司面试,面试官让你们写一个单例模式,你们把上面LZ给的代码写给面试官,如果你们是应届生,也许面试官会觉得不错,但如果你们已经是工作超过一年的同学,那么写出上面的代码恐怕你们就要完蛋。为什么呢?其实这是一个并发的问题,上面的代码在不考虑并发的情况下,确实没有问题,但是一旦考虑多线程并发,就会出现问题。
下面LZ用事实说话,给大家模拟一下多线程并发的情况
public class TestMyClass {
boolean myLock ;
public boolean isMyLock() {
return myLock;
}
public void setMyLock(boolean myLock) {
this.myLock = myLock;
}
public static void main(String[] args) throws Exception {
int num=100;
final CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
final Set<String> set=Collections.synchronizedSet(new HashSet<String>());
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<num;i++){
executorService.execute(new Runnable() {
public void run() {
try {
cyclicBarrier.await();
MyClass myClass = MyClass.getInstance();
set.add(myClass.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
Thread.sleep(2000);
System.out.println("------并发情况下我们取到的实例------");
for (String instance : set) {
System.out.println(instance);
}
executorService.shutdown();
}
}
代码比较简单,这里LZ是用的栅栏阻塞等待所有线程创建完毕,然后同时执行获取实例的操作。
LZ在程序中同时开启了100个线程来访问getInstance方法,然后把获得实例的实例字符串装入同步的set集合,这里为什么要放到set集合就不用LZ解释了吧=。=set集合会自动去重,所以我们看结果输出了多少实例字符串,就说明我们在并发访问的过程中产生了多少实例。
这里我让main线程睡眠了一次,是为了给足够的时间让100个线程全部开启。下面我们看一下结果(如果你照我的代码演示结果出现了一个,不要惊讶。我试了试大概3次之内就会出现我这种情况,甚至出现4个的都有)

那么为什么会造成这种情况呢?
当并发访问的时候,第一个调用getInstance方法的线程A,在判断完myClass是null的时候,线程A就进入了if块准备创造实例,说时迟那时快,在这同时另外一个线程B在线程A还未创造出实例之前,就又进行了myClass是否为null的判断,这时myClass当然依然为null,所以线程B也会进入if块去创造实例,那么问题就出来了,有两个线程都进入了if块去创造实例,结果就造成产生了两个对象出来。接下来LZ做的一个类似于图的东西,各位可以看看,虽然看起来不太直观,但是配合LZ的讲解详细各位一目了然。
1 public static MyClass getInstance(){ 对象的状态
2 public static MyClass getInstance(){ null
3 if(myClass ==null){ null
4 if(myClass ==null){ null
5 myClass = new MyClass(); object1
6 }
7 return myClass; object1
8 myClass = new MyClass(); object2
9 }
10 return myClass; object2
11 }
那么,我们又应该怎么解决这个线程并发导致的问题呢?
详细各位会立刻想起synchronized关键字,我们只要把getInstance()变成同步方法,就可以以上的问题了。
public class MyClass {
private static MyClass myClass;
private MyClass(){
}
public synchronized static MyClass getInstance(){
if(myClass ==null){
myClass = new MyClass();
}
return myClass;
}
}

通过加上synchronized关键字到getInstance()方法前,我们迫使每个线程在进入此方法前,必须先等待其他线程离开,就是说,不会有两个线程同时进入此方法。
但是,如果我们这样做,就会导致性能降低,因为,我们只有第一次调用getInstance()这个方法的时候需要同步,而当一旦设置好了myClass这个变量,我们就不需要再同步了,那么之后我们每次都同步,会导致性能降低。那么顺着这个角度去思考,我们可以先去判断myClass是否为null,当它为null时再同步。
public class MyClass {
private static MyClass myClass;
private MyClass(){
}
public static MyClass getInstance(){
if(myClass ==null){
synchronized(MyClass.class){
if(myClass ==null){
myClass = new MyClass();
}
}
}
return myClass;
}
}
这种做法也被称为双重加锁
经过刚才LZ的分析,这种做法应该是满足了要求,看起来是没有问题了,但如果我们再进一步深入考虑的话,其实仍然是有可能出现问题的。
这里我们深入到JVM中去探索上面这段代码,相信各位都知道虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,专业点说,创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。
我们先来搞清楚在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
我们假设2与3位置相反了,针对上述的双重加锁来讲,因为这时会先将内存地址赋给对象myClass,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为myClass对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了myClass,就会产生莫名的错误。
那么我们要如何避免这一个问题呢?我们可以给静态的实例属性加上关键字volatile,这样就不会出现实例化发生一半的情况,因为加入了volatile关键字,就等于禁止了JVM自动的指令重排序优化,并且强行保证线程中对变量所做的任何写入操作对其他线程都是即时可见的。volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作。由于本节我们讲的是设计模式,所以这里LZ不会去详细介绍volatile以及JVM中变量访问时所做的具体动作(或者以后LZ会单独将),感兴趣的读者可以去翻阅相关的资料。
另外由于volatile关键字是在JDK1.5版本出现的,所以凡是1.4及1.4之前的版本都无法使用。这里LZ把这种写法完整的列出来。
public class MyClass {
private volatile static MyClass myClass;
private MyClass(){
}
public static MyClass getInstance(){
if(myClass ==null){
synchronized(MyClass.class){
if(myClass ==null){
myClass = new MyClass();
}
}
}
return myClass;
}
}
另外,这就是我们常说的“懒汉式”,大家可以这样记“因为懒汉太懒了,所以只有用的时候才创建对象。”
懒汉式单例类。 只在外部对象第一次请求实例的时候才会去创建
优点:第一次调用时才会初始化,避免内存浪费。
缺点:必须加锁synchronized 才能保证单例,效率低
当然,除了这种写法,我们还有一种办法可以解决线程并发的问题,相信大家都听过“饿汉式”
class MyClassTo {
private static MyClassTo myClassTo = new MyClassTo();
private MyClassTo(){}
public static MyClassTo getInstance(){
return myClassTo;
}
}
因为太饿了,所以上来就创建=。=
饿汉式单例类。 它在类加载时就立即创建对象。
优点:没有加锁,执行效率高。 用户体验上来说,比懒汉式要好。
缺点:类加载时就初始化,浪费内存 那么为什么饿汉比懒汉要好,一个是空间换时间,一个是时间换空间,你们说是时间终于还是空间重要?=。=
另外,还有一种单例模式,被称为"登记式"
class MyClassThree{
private MyClassThree(){}
public static MyClassThree getInstance(){ return SINGLETON.myClassThree;}
private static class SINGLETON{//内部类
private static final MyClassThree myClassThree= new MyClassThree();
}
}
内部类只有在外部类被调用才加载,产生SINGLETON实例,又不用加锁,这个模式有上述俩模式的优点,屏蔽了他们的缺点,是最好的单例模式。
首先来说一下,这种方式为何会避免了双重加锁的漏洞,主要是因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们根本无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。
那么我们总结一下这种模式帮助我们做到了什么:
1.在不考虑反射强行突破访问限制的情况下,MyClassThree最多只有一个实例。
2.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。
3.保证了并发访问的情况下,不会发生由于并发而产生多个实例。
好了,到这里单例模式LZ就讲完了,下期预告,等下次再说=。=
JAVA设计模式详解(四)----------单例模式的更多相关文章
- android java 设计模式详解 Demo
android java 设计模式详解 最近看了一篇设计模式的文章,深得体会,在此基础我将每种设计模式的案例都写成Demo的形式,方便读者研究学习, 首先先将文章分享给大家: 设计模式(Design ...
- JAVA设计模式详解(五)----------适配器模式
各位朋友好,本章节我们继续讲第五个设计模式. 在生活中,我们都知道手机内存卡是无法直接接电脑的,因为内存卡的卡槽比较小,而电脑只有USB插孔,此时我们需要用到读卡器.这个读卡器就相当于是适配器.这是生 ...
- JAVA设计模式详解(三)----------装饰者模式
今天LZ带给大家的是装饰者模式,提起这个设计模式,LZ心里一阵激动,这是LZ学习JAVA以来接触的第一个设计模式,也许也是各位接触的第一个设计模式.记得当初老师在讲IO的时候就提到过它:“是你还有你, ...
- JAVA设计模式详解(六)----------状态模式
各位朋友,本次LZ分享的是状态模式,在这之前,恳请LZ解释一下,由于最近公司事情多,比较忙,所以导致更新速度稍微慢了些(哦,往后LZ会越来越忙=.=). 状态模式,又称状态对象模式(Pattern o ...
- JAVA设计模式详解(二)----------观察者模式
有一个模式可以帮助你的对象知悉现况,不会错过该对象感兴趣的事,对象甚至在运行时可以决定是否要继续被通知,如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式 .观察者模式 ...
- JAVA设计模式详解(一)----------策略模式
策略模式,顾名思义就是设计一个策略算法,然后与对象拆分开来将其单独封装到一系列策略类中,并且它们之间可以相互替换.首先LZ举一个例子为大家引出这一个模式. 例子:某公司的中秋节奖励制度为每个员工发放2 ...
- [ 转载 ] Java开发中的23种设计模式详解(转)
Java开发中的23种设计模式详解(转) 设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类 ...
- Java温故而知新(5)设计模式详解(23种)
一.设计模式的理解 刚开始“不懂”为什么要把很简单的东西搞得那么复杂.后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精髓所在,我所理解的“简单”就是一把钥匙开一把锁的模式,目 ...
- JAVA设计模式简介及六种常见设计模式详解
一.什么是设计模式 ...
随机推荐
- Loop List
Loop List is very common in interview. This article we give a more strict short statement about its ...
- Linux的1000个命令
目录 Linux常用命令 uptime wget uname free who last history pwd cd ls cat head tail tr wc cut diff touch mk ...
- [HAOI2018]染色(容斥+NTT)
补充一篇详细得不能再详细的题解,比如让我自己看懂. 可能与前面的题解有些相同,我想补充一下自己的想法. 显然,最多 \(K\) 最大为 \(N=min(\lfloor \frac nS\rfloor, ...
- cad2019卸载/安装失败/如何彻底卸载清除干净cad2019注册表和文件的方法
cad2019提示安装未完成,某些产品无法安装该怎样解决呢?一些朋友在win7或者win10系统下安装cad2019失败提示cad2019安装未完成,某些产品无法安装,也有时候想重新安装cad2019 ...
- 设计模式《JAVA与模式》之解释器模式
在阎宏博士的<JAVA与模式>一书中开头是这样描述解释器(Interpreter)模式的: 解释器模式是类的行为模式.给定一个语言之后,解释器模式可以定义出其文法的一种表示,并同时提供一个 ...
- widows下 python环境变量配置
widows下 python环境变量配置 便于cmd命令行操作,例如:直接进入Python解释器环境.使用pip安装模块等.
- 监督学习——决策树理论与实践(下):回归决策树(CART)
介绍 决策树分为分类决策树和回归决策树: 上一篇介绍了分类决策树以及Python实现分类决策树: 监督学习——决策树理论与实践(上):分类决策树 决策树是一种依托决策而建立起来的一种 ...
- Spring4 mvc+maven 框架搭建(3)
经过前面两个环节,spring mvc的原料已经准备好了,现在就可以正式开始搭建springmvc框架了. 首先先介绍介绍搭建的框架具有的功能: 1)集成log4j,配置好日志相关并可以打印出相关的日 ...
- 微信web开发者工具、破解文件、开发文档和开发Demo下载
关注,QQ群,微信应用号社区 511389428 下载: Win: https://pan.baidu.com/s/1bHJGEa Mac: https://pan.baidu.com/s/1slhD ...
- SpringSecurity学习之基于数据库的用户认证
SpringSecurity给我们提供了一套最基本的认证方式,可是这种方式远远不能满足大多数系统的需求.不过好在SpringSecurity给我们预留了许多可扩展的接口给我们,我们可以基于这些接口实现 ...