上一章我们学习了装饰者模式,这章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设计模式详解(四)----------单例模式的更多相关文章

  1. android java 设计模式详解 Demo

    android java 设计模式详解 最近看了一篇设计模式的文章,深得体会,在此基础我将每种设计模式的案例都写成Demo的形式,方便读者研究学习, 首先先将文章分享给大家: 设计模式(Design ...

  2. JAVA设计模式详解(五)----------适配器模式

    各位朋友好,本章节我们继续讲第五个设计模式. 在生活中,我们都知道手机内存卡是无法直接接电脑的,因为内存卡的卡槽比较小,而电脑只有USB插孔,此时我们需要用到读卡器.这个读卡器就相当于是适配器.这是生 ...

  3. JAVA设计模式详解(三)----------装饰者模式

    今天LZ带给大家的是装饰者模式,提起这个设计模式,LZ心里一阵激动,这是LZ学习JAVA以来接触的第一个设计模式,也许也是各位接触的第一个设计模式.记得当初老师在讲IO的时候就提到过它:“是你还有你, ...

  4. JAVA设计模式详解(六)----------状态模式

    各位朋友,本次LZ分享的是状态模式,在这之前,恳请LZ解释一下,由于最近公司事情多,比较忙,所以导致更新速度稍微慢了些(哦,往后LZ会越来越忙=.=). 状态模式,又称状态对象模式(Pattern o ...

  5. JAVA设计模式详解(二)----------观察者模式

    有一个模式可以帮助你的对象知悉现况,不会错过该对象感兴趣的事,对象甚至在运行时可以决定是否要继续被通知,如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式 .观察者模式 ...

  6. JAVA设计模式详解(一)----------策略模式

    策略模式,顾名思义就是设计一个策略算法,然后与对象拆分开来将其单独封装到一系列策略类中,并且它们之间可以相互替换.首先LZ举一个例子为大家引出这一个模式. 例子:某公司的中秋节奖励制度为每个员工发放2 ...

  7. [ 转载 ] Java开发中的23种设计模式详解(转)

    Java开发中的23种设计模式详解(转)   设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类 ...

  8. Java温故而知新(5)设计模式详解(23种)

    一.设计模式的理解 刚开始“不懂”为什么要把很简单的东西搞得那么复杂.后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精髓所在,我所理解的“简单”就是一把钥匙开一把锁的模式,目 ...

  9. JAVA设计模式简介及六种常见设计模式详解

    一.什么是设计模式                                                                                           ...

随机推荐

  1. abstract抽象

    abstract:抽象 是用来修饰抽象类和抽象方法的 那么什么抽象,抽象有究竟有什么用呢?? 我们知道,“类”是某一类具有相同特征或行为的物事,是将这些物事特征向上抽取得来的:“父类”也是子类不断向上 ...

  2. SpringCloud之Eureka集群

    前面我们介绍了SpringCloud注册中心Eureka,但是存在一个单点故障的问题,一个注册中心远远不能满足实际的生产环境,现在我们介绍一下如何搭建一个Eureka集群. 一:集群环境搭建 我们先建 ...

  3. HTTP的请求报文与响应报文

    报文: 简单来说,报文就是也就是HTTP报文,作用是在各个系统之间进行和响应时用来交换与传输的数据单元,即站点一次性要发送的数据块,这些数据块以一些文本形式的元信息开头,这些信息描述了报文的内容及含义 ...

  4. python实用库:PrettyTable 学习

    python实用库:PrettyTable 学习 PrettyTable说明 PrettyTable 是python中的一个第三方库,可用来生成美观的ASCII格式的表格,十分实用. 以下为官方介绍: ...

  5. (转)Python3 模块3之 Urllib之 urllib.parse、urllib.robotparser

    原文:https://blog.csdn.net/qq_36148847/article/details/79153738 https://blog.csdn.net/zly412934578/art ...

  6. Android开发艺术探索学习笔记(一)

    第一章 Activity的生命周期和启动模式 1.1Activity的生命周期全面解析 1.1.1典型情况下的生命周期分析 (1)在两个Activity进行切换时,当前的Activity的onPaus ...

  7. php -- 日期时间

    ----- 017-datetime.php ----- <!DOCTYPE html> <html> <head> <meta http-equiv=&qu ...

  8. java-jmx使用

    先粘一段内容 .程序初哥一般是写死在程序中,到要改变的时候就去修改代码,然后重新编译发布. .程序熟手则配置在文件中(JAVA一般都是properties文件),到要改变的时候只要修改配置文件,但还是 ...

  9. centos7.0安装docker-18.06.1-ce不能启动问题

    最近用centos7.0 yum安装了一个docker-ce18.06.1  但是发现安装好不能启动,于是上官网看了一下,说是docker-ce18.06.1是从centos7.2开始支持的,但是7. ...

  10. Redis开发与运维

    常用命令 redis-server启动redis redis-server /opt/redis/redis.conf    配置启动 redis-server --port 6379 --dir / ...