当前位置: 首页 > news >正文

单例模式浅析

  单例模式 

  单例模式是一种比较常见的模式,看起来很简单,但是要做到高效安全的使用,其中还是有很多要点的。参考了Head First及众多网友的文章,稍微总结一下,以备查看。

  单例模式的定义:确保一个类只有一个实例,并且提供一个全局访问点。

1. 最简单的单例(饿汉模式),程序一加载就对 instance 变量进行初始化,好处是简单明了,缺点:如果初始化的耗费的资源特别多,而之后这个类可能未被使用到,就会造成浪费。

public class Singleton{
    // 单例类静态引用
    private static Singleton instance = new Singleton();  
    // 私有构造方法
    private Singleton(){}  
    // 静态方法:全局访问点
    public static Singleton getInstance(){  
        return instance;  
    }  
}  

 

2. 延迟加载(懒汉模式),作为改进,我们可以在使用到的时候才去创建单例类。

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        // 使用的时候去检测是否已经初始化
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

  这种方式实现的单例是线程非安全的,假设有线程 A 和 B,同时去获取 LazySingleton 实例,当 A 线程进入 getInstance(),执行到 if (instance == null) 时,此时判断结果为真;之后切换到线程 B 执行,此时也执行到 if (instance == null) ,此时判断结果为真,会对 instance 变量进行初始化;此时切换回线程 A ,根据之前的判断结果,也会对 instance 变量进行初始化,此时就会得到两个不一样的实例,单例失败。

  为了实现线程安全,只要在 getInstance() 方法上添加 synchronized 关键字即可。

 

3. 双重校验锁

  加上 synchronized 关键字,实现了线程安全,但又带来了性能问题。通过同步方法会让性能大幅下降。

  通过代码我们可以看到,instance 变量在整个程序执行期间只会初始化一次,如果只为了这一次初始化,每次获取单例对象都必须在synchronized这里进行排队,实在太损耗性能。为此我们可以如下改进:① instance 变量只会初始化一次,把 synchronized 关键字加载此处;② 为了避免每次获取单例对象都在同步代码上等待,可以在同步代码块外层再加一次 instance == null 判断。

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        // 两层校验才能确保单例
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

 

4. volatile 关键字

   Java中的指令重排优化是指在不改变原语义(即保证运行结果不变)的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。这种优化在单线程中是没有任何问题的,但是在多线程中就会出现问题。

  创建一个变量需要2个步骤:一个是申请一块内存,调用构造方法进行初始化操作;另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。假设线程A开始创建 SingletonClass 的实例,此时线程 B 调用了 getInstance() 方法,首先判断 instance 是否为 null。按照我们上面所说的内存模型,A 已经把 instance 指向了那块内存,只是还没有调用构造方法,因此 B 检测到 instance 不为 null,于是直接把 instance 返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!

  在 JDK1.5 及之后版本赋予了volatile关键字明确用法。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。所以只需要在 instance 变量前加上 volatile 修饰符即可避免Java指令优化带来的问题

public class DoubleCheckSingleton {

    private static volatile DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        // 两层校验才能确保单例
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

注:volatile 关键字用法

  ① 可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中(工作内存是线程独享的,主存是线程共享的)

  ② 禁止指令重排序优化。

 

5. 静态内部类

  上述 volatile 关键字收到 JDK 版本限制,如下的静态内部类实现方式是确保了延迟加载和线程安全的。

public class StaticInnerSingleton {

    private static class Holder {
        private static StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getSingleton() {
        return Holder.instance;
    }
}

  可以把 StaticInnerSingleton 实例放到一个静态内部类中,这样就避免了静态实例在 StaticInnerSingleton 类加载的时候就创建对象,并且由于静态内部类只会被加载一次,而类的构造必须是原子性的,非并发的,所以这种写法也是线程安全的

  在上述代码中,因为 StaticInnerSingleton 没有 static 修饰的属性,因此并不会被初始化。直到调用 getInstance() 的时候,会首先加载 Holder

类,这个类有一个 static 的 StaticInnerSingleton 实例,因此需要调用 StaticInnerSingleton 的构造方法,然后 getInstance() 将把这个内部类的instance 返回给使用者。由于这个 instance 是 static 的,因此并不会构造多次。 

 

6. 枚举式单例

public enum EnumSingleton {

    INSTANCE;
    
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public String getSomething() {
        return "String";
    }
}

  使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》书中推荐使用的方法。

 

参考: 深入Java单例模式  http://devbean.blog.51cto.com/448512/203501/

    你真的会写单例模式吗--Java实现 http://www.importnew.com/18872.html 

    感谢二位作者文章,本文仅作整理学习,侵删!   

转载于:https://www.cnblogs.com/techroad4ca/p/6227943.html

相关文章:

  • Django基于Pycharm开发之二 [使用django adminSite]
  • bodyParser中间件的研究
  • asp.net mvc adminlte第一波
  • 【转】TCP/IP协议栈及OSI参考模型详解
  • 闭包2
  • 【Kubernetes】K8S 网络隔离 方案
  • 一款实时性能监测工具
  • 张小龙微信小程序演讲内容简介
  • (转)socket Aio demo
  • maven log4g 用法
  • JPA 系列教程 异常 集锦
  • react自学笔记总结不间断更新
  • 延迟构造
  • 微信小程序简易教程
  • Mysql免安装版配置【图文版和文字版】
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • Effective Java 笔记(一)
  • ES6系统学习----从Apollo Client看解构赋值
  • Mithril.js 入门介绍
  • Spring Cloud中负载均衡器概览
  • 简单易用的leetcode开发测试工具(npm)
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 蓝海存储开关机注意事项总结
  • 力扣(LeetCode)21
  • 微信开源mars源码分析1—上层samples分析
  • 微信小程序实战练习(仿五洲到家微信版)
  •  一套莫尔斯电报听写、翻译系统
  • Python 之网络式编程
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • (1)虚拟机的安装与使用,linux系统安装
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (4.10~4.16)
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (超详细)语音信号处理之特征提取
  • (附源码)ssm高校实验室 毕业设计 800008
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • .axf 转化 .bin文件 的方法
  • .md即markdown文件的基本常用编写语法
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .Net FrameWork总结
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NetCore项目nginx发布
  • .net反编译的九款神器
  • .net中应用SQL缓存(实例使用)
  • .sh文件怎么运行_创建优化的Go镜像文件以及踩过的坑
  • :中兴通讯为何成功
  • @html.ActionLink的几种参数格式
  • [2016.7 Day.4] T1 游戏 [正解:二分图 偏解:奇葩贪心+模拟?(不知如何称呼不过居然比std还快)]
  • [BUG] Authentication Error
  • [CTSC2014]企鹅QQ
  • [iOS]-网络请求总结
  • [JavaScript] JavaScript事件注册,事件委托,冒泡,捕获,事件流
  • [Linux] Apache的配置与运用