一、简单描述

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意看这里

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

分类

  • 单例模式就实例的创建时机来划分可分为:懒汉式饿汉式两种。

举个小例子

  • 高中住校,北方的冬天比较凉爽,那个时候我的班主任老师“非常负责”,为了我们早上能够早起“1,2,1”(出操),每天早上五点半“从天而降”,二话不说先掀被子(零下好几度的天气你敢想有多酸爽)。
  • 好了不吹牛逼了重点来了!!!如果班主任还没来,我们自己走出温柔乡了,这种情况在单例模式中称之为饿汉式。(没有外力干预的情况下,自己就把实例创建好了)
  • 如果班主任把你被子掀了,一股妖风把你吹醒,这种情况在单例模式中称之为为懒汉式
    (班主任都来了你还不起,你不是懒汉是啥?你不挨冻谁挨冻。在外部力量的干预下,才把实例建立起来。)

二、优劣分析

懒汉式优点:

  • 实例只有在被使用的时候才创建,可以节约系统资源,体现了延迟加载的思想。
  • 延迟加载:开始的时候不加载资源,一直到要使用这个资源的时候,才去加载,这样可以尽可能的节省系统资源。

懒汉式缺点

  • 由于系统刚启动时且未被外部调用时,实例没有创建;如果一时间有多个线程同时调用LazySingleton.getLazyInstance()方法很有可能会产生多个实例。
  • 也就是说懒汉式在多线程下,是不能保持单例实例的唯一性的,要想保证多线程下的单例实例的唯一性,就要用同步,同步会导致多线程下争夺锁资源,运行效率不高。

饿汉式优点

  • 写法简单,在多线程下也能保证单例实例的唯一性,不用同步,运行效率高。

饿汉式缺点

  • 在外部没有调用该类的时候,该类的实例就创建了,如果该类实例的创建比较消耗系统资源,并且外部一直没有调用该实例,那么这部分的系统资源不就白白消耗了。


三、代码实例(干货干货)

懒汉式单例实例:

package singleton;/*** * 懒汉式单例**/
public class LazySingLeton {//私有化构造函数,防止在该类外部通过new的形式创建实例private LazySingLeton(){System.out.println("生成LazySingLeton实例一次");}//私有的 静态的实例 设置为私有的,防止外部直接访问该实例变量,设置为静态的 说明该实例是LazySingLeton类型的唯一//若开始时,没有调用访问实例的方法 那么实例就不会自己创建private static LazySingLeton lazyInstance=null;//公有的访问单例实例的方法 当外部调用访问该实例的方法时 才能被 创建public  static LazySingLeton getLazyInstance(){//若实例还没有被创建,则创建实例;若实例已经被创建则直接返回之前 创建的实例,即不会返回两个实例if(lazyInstance==null){lazyInstance=new LazySingLeton();}return lazyInstance;}  //测试public static void main(String[] args) {LazySingLeton lazySingLeton=LazySingLeton.getLazyInstance();LazySingLeton lazySingLeton1=LazySingLeton.getLazyInstance();LazySingLeton lazySingLeton2=LazySingLeton.getLazyInstance();}
}------------控制台输出------------生成LazySingLeton实例一次

饿汉式单例实例:

package singleton;/*** * 饿汉式**/
public class NoLazySingLeton {//私有化构造函数,防止在该类外部通过new的形式创建实例private NoLazySingLeton(){System.out.println("创建NoLazySingLeton实例一次");}//私有的 静态的实例,设置为私有的防止外部直接访问该实例变量,说明该实例时LazySingLeton实例的唯一的//当系统加载NoLazySingLeton类文件的时候,就创建了该类的实例private static NoLazySingLeton instance=new NoLazySingLeton();//共有的访问单例实例的方法public static NoLazySingLeton getInstance(){return instance;}//测试public static void main(String[] args) {NoLazySingLeton noLazySingLeton=NoLazySingLeton.getInstance();NoLazySingLeton noLazySingLeton1=NoLazySingLeton.getInstance();NoLazySingLeton noLazySingLeton2=NoLazySingLeton.getInstance();    }------------控制台输出------------创建NoLazySingLeton实例一次}

线程不安全懒汉式单例 实例:

懒汉式在多线程环境下面是有问题的,下面演示这个多线程环境下很有可能出现的问题:

package singleton;/*** * 懒汉式单例**/
public class LazySingLeton {//私有化构造函数,防止在该类外部通过new的形式创建实例private LazySingLeton(){System.out.println("生成LazySingLeton实例一次");}//私有的 静态的实例 设置为私有的,防止外部直接访问该实例变量,设置为静态的 说明该实例是LazySingLeton类型的唯一//若开始是,没有调用访问实例的方法 那么实例就不会自己创建private static LazySingLeton lazyInstance=null;//公有的访问单例实例的方法 当外部调用访问该实例的方法时 才能被 创建public  static LazySingLeton getLazyInstance(){//若实例还没有被创建,则创建实例;若实例已经被创建则直接返回之前 创建的实例,即不会返回两个实例if(lazyInstance==null){lazyInstance=new LazySingLeton();}return lazyInstance;}//我们新建10个线程,让这10个线程同时调用LazySingleton.getLazyInstance()方法   public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(){@Overridepublic void run() {LazySingleton.getLazyInstance();}}.start();}}------------控制台输出------------生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!
生成LazySingleton实例一次!}

控制台输出了10次,表示懒汉式单例模式在10个线程同时访问的时候,创建了10个实例,这足以说明懒汉式单例在多线程下已不能保持其实例的唯一性。

为什么多线程下懒汉式单例会失效?

  • 如果2个线程同时访问上面的懒汉式单例,现在有两个线程A和B同时访问LazySingleton.getLazyInstance()方法。
  • 假设A先得到CPU的时间切片,A执行到21行处 if (lazyInstance == null) ,由于lazyInstance 之前并没有实例化,所以lazyInstance == null为true,在还没有执行22行创建实例的时候
  • 此时CPU将执行时间分给了线程B,线程B执行到21行处 if (lazyInstance == null) 时,由于lazyInstance 之前并没有实例化,所以lazyInstance == null为true,线程B继续往下执行实例的创建过程,线程B创建完实例之后,返回。
  • 此时CPU将时间切片分给线程A,线程A接着开始执行22行实例的创建,实例创建完之后便返回。由此看线程A和线程B分别创建了一个实例(存在2个实例了),这就导致了单例的失效。

线程安全懒汉式单例 实例:

如何让 懒汉式单例在多线程下发挥正确的作用呢?那就是在访问单例实例的方法处进行同步

package singleton;public class SafeLazySingLeton {private SafeLazySingLeton(){System.out.println("生成SafeLazySingLeton实例一次");}private static SafeLazySingLeton instance=null;//对整个访问实例的方法进行同步/*** 整个方法用了synchronized 关键字进行方法同步,这个缺点很是明显,* 就是锁的粒度太大,很多线程同时访问的时候导致阻塞很严重。* */public synchronized static SafeLazySingLeton getInstance(){if(instance == null){instance=new SafeLazySingLeton();}return instance;}//对必要的代码块进行同步public static SafeLazySingLeton getInstance1(){if(instance == null){synchronized(SafeLazySingLeton.class){if(instance ==null ){instance=new SafeLazySingLeton();}}}return instance; }//测试public static void main(String[] args) {SafeLazySingLeton safeLazySingLeton=SafeLazySingLeton.getInstance();SafeLazySingLeton safeLazySingLeton1=SafeLazySingLeton.getInstance();SafeLazySingLeton safeLazySingLeton2=SafeLazySingLeton.getInstance();SafeLazySingLeton safeLazySingLeton3=SafeLazySingLeton.getInstance1();SafeLazySingLeton safeLazySingLeton4=SafeLazySingLeton.getInstance1();SafeLazySingLeton safeLazySingLeton5=SafeLazySingLeton.getInstance1();}------------控制台输出------------生成SafeLazySingLeton实例一次}

对方法同步:

  • 上面的实现 在12行对访问单例实例的整个方法用了synchronized 关键字进行方法同步,这个缺点很是明显,就是锁的粒度太大,多个线程同时访问的时候导致阻塞很严重。

对代码块同步:

  • 在18行的方法getInstance1中,只是对必要的代码块使用了synchronized关键字,注意由于方法时static静态的,所以监视器对象是SafeLazySingleton.class
  • 同时我们在19行和21行,使用了实例两次非空判断,一次在进入synchronized代码块之前,一次在进入synchronized代码块之后

肯定有小伙伴这样想:既然19行进行了实例非空判断了,进入synchronized代码块之后就不必再次进行非空判断了,如果这样做的话,会导致什么问题?我们来分析一下:

  • 同样假设我们有两个线程A和B,A获取CPU时间片段,在执行到19行时,由于之前没有实例化,所以instance == null 为true,然后A获得监视器对象SafeLazySingleton.class的锁,A进入synchronized代码块里面;

  • 与此同时线程B执行到19行,此时线程A还没有执行实例化动作,所以此时instance == null 为true,B想进入同步块,但是发现锁在线程A手里,所以B只能在同步块外面等待。此时线程A执行实例化动作,实例化结束之后,返回该实例。

  • 随着线程A退出同步块,A也释放了锁,线程B就获得了该锁,若此时不进行第二次非空判断,会导致线程B也实例化创建一个实例,然后返回自己创建的实例,这就导致了2个线程访问创建了2个实例,导致单例失效。若进行第二次非空判断,发现线程A已经创建了实例,instance == null已经不成立了,则直接返回线程A创建的实例,这样就避免了单例的失效。

有细心的网友会发现即便去掉19行非空判断,多线程下单例模式一样有效:

  • 线程A获取监视器对象的锁,进入了同步代码块,if(instance == null) 成立,然后A创建了一个实例,然后退出同步块,返回。这时在同步块外面等待的线程B,获取了锁进入同步块,执行if(instance == null)发现instance已经有值了不再是空了,然后直接退出同步块,返回。

  • 既然去掉19行,多线程下单例模式一样有效,那为什么还要有进入同步块之前的非空判断(19行)?这应该主要是考虑到多线程下的效率问题:

  • 我们知道使用synchronized关键字进行同步,意味着就是独占锁,同一时刻只能有一个线程执行同步块里面的代码,还要涉及到锁的争夺、释放等问题,是很消耗资源的。单例模式,构造函数只会被调用一次。如果我们不加19行,即不在进入同步块之前进行非空判断,如果之前已经有线程创建了该类的实例了,那每次的访问该实例的方法都会进入同步块,这会非常的耗费性能.如果进入同步块之前加上了非空判断,发现之前已经有线程创建了该类的实例了,那就不必进入同步块了,直接返回之前创建的实例即可。这样就基本上解决了线程同步导致的性能问题。

多线程下单例的优雅的解决方案:

上面的实现使用了synchronized同步块,并且用了双重非空校验,这保证了懒汉式单例模式在多线程环境下的有效性,但这种实现感觉还是不够好,不够优雅。

下面介绍一种优雅的多线程下单例模式的实现方案:


package singleton;public class GracefulSingleton {private GracefulSingleton(){System.out.println("创建GracefulSingleton实例一次!");}//类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载private static class SingletonHoder{//静态初始化器,由JVM来保证线程安全private static GracefulSingleton instance = new GracefulSingleton();}public static GracefulSingleton getInstance(){return SingletonHoder.instance;}
}

上面的实现方案使用一个内部类来维护单例类的实例,当GracefulSingleton被加载的时候,其内部类并不会被初始化,所以可以保证当GracefulSingleton被装载到JVM的时候,不会实例化单例类,当外部调用getInstance方法的时候,才会加载内部类SingletonHoder,从而实例化instance,同时由于实例的建立是在类初始化时完成的,所以天生对多线程友好,getInstance方法也不需要进行同步。

  • 单例模式本质上是控制单例类的实例数量只有一个,有些时候我们可能想要某个类特定数量的实例,这种情况可以看做是单例模式的一种扩展情况。比如我们希望下面的类SingletonExtend只有三个实例,我们可以利用Map来缓存这些实例。
package singleton;import java.util.HashMap;
import java.util.Map;public class SingletonExtend {//装载SingletonExtend实例的容器private static final Map<String,SingletonExtend> container = new HashMap<String, SingletonExtend>();//SingletonExtend类最多拥有的实例数量private static final int MAX_NUM = 3;//实例容器中元素的key的开始值private static String CACHE_KEY_PRE = "cache";private static int initNumber = 1;private SingletonExtend(){System.out.println("创建SingletonExtend实例1次!");}//先从容器中获取实例,若实例不存在,在创建实例,然后将创建好的实例放置在容器中public static SingletonExtend getInstance(){String key = CACHE_KEY_PRE+ initNumber;SingletonExtend singletonExtend = container.get(key);if (singletonExtend == null) {singletonExtend = new SingletonExtend();container.put(key,singletonExtend);}initNumber++;//控制容器中实例的数量if (initNumber > 3) {initNumber = 1;}return singletonExtend;}public static void main(String[] args) {SingletonExtend instance = SingletonExtend.getInstance();SingletonExtend instance1 = SingletonExtend.getInstance();SingletonExtend instance2 = SingletonExtend.getInstance();SingletonExtend instance3 = SingletonExtend.getInstance();SingletonExtend instance4 = SingletonExtend.getInstance();SingletonExtend instance5 = SingletonExtend.getInstance();SingletonExtend instance6 = SingletonExtend.getInstance();SingletonExtend instance7 = SingletonExtend.getInstance();SingletonExtend instance8 = SingletonExtend.getInstance();SingletonExtend instance9 = SingletonExtend.getInstance();System.out.println(instance);System.out.println(instance1);System.out.println(instance2);System.out.println(instance3);System.out.println(instance4);System.out.println(instance5);System.out.println(instance6);System.out.println(instance7);System.out.println(instance8);System.out.println(instance9);}------------控制台输出------------创建SingletonExtend实例1次!创建SingletonExtend实例1次!创建SingletonExtend实例1次!singleton.SingletonExtend@3a3ee284singleton.SingletonExtend@768965fbsingleton.SingletonExtend@36867e89singleton.SingletonExtend@3a3ee284singleton.SingletonExtend@768965fbsingleton.SingletonExtend@36867e89singleton.SingletonExtend@3a3ee284singleton.SingletonExtend@768965fbsingleton.SingletonExtend@36867e89singleton.SingletonExtend@3a3ee284从控制台输出情况可以看到 我们成功的控制了SingletonExtend的实例数据只有三个}

四、小结

读完全篇我们知道,传统的饿汉式单例无论在单线程还是多线程环境下都是线程安全的,但是传统的懒汉式单例在多线程环境下是非线程安全的。

要想实现效率高的线程安全的单例,我们必须注意以下两点:

  • 尽量减少同步块的作用域;

  • 尽量使用细粒度的锁。


文章仅用来作记录分享,以上纯属个人见解,如有不当之处,还望指正。

最近开通个人了微信公众号,以后将会定期分享工作学习过程中遇到的问题,欢迎关注与我一道成长交流~:

微信搜索: MYY668999程序猿爱篮球 即可上车。

Java设计模式之单例模式(附简单案例)相关推荐

  1. java 向nodejs 发送请求简单案例

    java  向nodejs 发送请求简单案例 最近在做nodejs的东西,一直不明白java如何与nodejs建立连接,下面写了一个简单demo: nodejs端: var http = requir ...

  2. Java设计模式之单例模式(七种写法)

    Java设计模式之单例模式(七种写法) 第一种,懒汉式,lazy初始化,线程不安全,多线程中无法工作: public class Singleton {private static Singleton ...

  3. Java设计模式之单例模式的学习

    本篇是本人的第二篇博客 旨在记录本人对于Java设计模式之单例模式的学习和理解,也希望本篇可以对一些正在学习的小伙伴起到一些帮助 单例模式(singleton)的特点: 1.单例模式有且仅有一个实例: ...

  4. java设计模式之单例模式(七种方法)

    单例模式:个人认为这个是最简单的一种设计模式,而且也是在我们开发中最常用的一个设计模式. 单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个 ...

  5. java设计模式之——单例模式(八种实现)

    一.介绍 有时,我们需要某个类的实例始终只有一个,举个例子,如果用面向对象语言写的操作系统,那么桌面这个实例肯定就只有一个,无论从哪个地方进入的桌面,都是同一个. 所谓类的单例设计模式,就是采取一定的 ...

  6. Java 设计模式(3)单例模式

    前言 概念: java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自 ...

  7. Java设计模式之单例模式(Singleton Pattern)

    **单例模式:用来创造独一无二的,只能有一个实例的对象设计模式.单例模式确保一个类只有一个实例,并提供一个全局访问点.**相比于全局变量(对对象的静态引用),单例模式可以延迟实例化,而且全局变量不能保 ...

  8. Java设计模式(二)简单工厂模式—设计模式六大原则

    文章目录 设计模式六大原则 1. 开闭原则 2. 里氏代换原则 3. 依赖倒转原则 4. 接口隔离原则 5. 迪米特法则(最少知道原则) 6. 合成复用原则 工厂设计模式 什么是工厂模式 工厂模式的好 ...

  9. 设计模式(一)—单例模式(附Java代码)

    单例模式(Singleton Pattern):采取一定的方法保证在整个的软件系统中,对于某个类只能存在一个对象实例,并且该类只提供一个取得其实例对象的方法. 比如Hibernate的SessionF ...

最新文章

  1. 对2020年Linux和开源的5个大胆预测
  2. SAP 启用了HUM和QM的前提下,无法对采购订单的收货在质量放行前执行部分退货!
  3. ML/DL之激活函数/求导函数:ML中常用的AF激活函数(step_function、sigmoid、softmax、ReLU等)求导函数等代码实现之详细攻略
  4. 九大经典算法之选择排序、堆排序
  5. 联系人排序java代码_Android仿微信联系人按字母排序
  6. linux ntfs 介绍
  7. STC学习:导航按键
  8. 华为十年架构师实战经验总结:大规模分布式系统架构与设计实战
  9. 网络知识:光猫光纤宽带故障排查笔记!
  10. 电子合同的风险有哪些?小心别被坑了
  11. python实现爬取网易云音乐评论,并且将评论信息存储到pymysql
  12. python读取海康视频流(rtsp格式)
  13. 全触控HIFI级音质,击音Super HD II,你喜欢的样子我都有!
  14. Java使用winrar压缩和解压缩文件
  15. matlab求x对应y值,matlab 不知道函数表达式,已知y值求x
  16. 第六章 网络学习相关技巧1(最优路径梯度)
  17. 核磁T1加权像和T2加权像的区别
  18. [原创] 利用busybox, extlinux 在工控机CompactFlash(CF卡)上构建Linux系统(上)
  19. CSS或JS实现逐帧动画方案
  20. 不管你对科技有没有兴趣,这十家公司一定会让你会心一笑 | 2018年度总结

热门文章

  1. fateskins CSGO饰品皮肤网页开箱子网站
  2. [转]Pico Neo 2✨五、实现Pico到电脑的投屏
  3. 【参赛作品4】Day2:openGauss安装
  4. 深入扒一扒 NumPy 中文网《防脱发指南》,糟糕被圈粉了!
  5. Packer 自动化镜像 Windows 安装过程
  6. 难上加难!消费金融公司如何做出调整
  7. Java 递归取Children
  8. VS Code - 自动保存
  9. Ubuntu 修改鼠标灵敏度
  10. lisp批量生成轴线_已知大量坐标和直径,请问如何以用autocad 的lisp 以坐标为球心批量画球...