结合JDK源码看设计模式——单例模式
定义:
保证一个类仅有一个实例,并提供一个全局访问点
适用场景:
确保任何情况下这个对象只有一个实例
详解:
- 私有构造器
- 单利模式中的线程安全+延时加载
- 序列化和反序列化安全,
- 防止反射攻击
- 结合JDK源码分析设计模式
1.私有构造器:
将本类的构造器私有化,其实这是单例的一个非常重要的步骤,没有这个步骤,可以说你的就不是单例模式。这个步骤其实是防止外部函数在new的时候能构造出来新的对象,我们说单例要保证一个类只有一个实例,如果外部能new新的对象,那我们单例就是失败的。所以无论什么时候一定要将这个构造器私有化
2.单例模式中的线程安全+延时加载(懒汉式):
其实从单线程角度来看,懒汉式是安全。这里我们先来介绍一个线程安全的懒汉式接下来我们从三个版本的懒汉式来分析如何即做到线程安全又做到效率提高
2.1原始版本
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
我们来稍微分析一下为什么线程不安全,现在有A,B两个线程,假设两个线程同时都走到了lazySingleton = new LazySingleton();这个创建对象的行,当都执行完的时候,就会创建两个不同的对象然后分别返回。所以违背了单例模式的定义
2.2加锁
可能很多人会直接在getInstance()方法上加一个synchronize关键字,这样做完全可以但是效率会较慢,因为synchronize相当于锁了整个对象,下面的双锁结构就会比较轻量级一点
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){ }
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
可能很多人一眼就看见synchronize关键字位置变换了,锁的范围变小了,但是最关键的一个是private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;中的volatile关键字,因为如果不加这个关键字的时候,JVM会对没有依赖关系的语句进行重排序,就是可能会在线程A的时候底层先设置lazyDoubleCheckSingleton 指向刚分配的内存地址,然后再来初始化对象,线程B呢在线程A设置lazyDoubleCheckSingleton 指向刚分配的内存地址完后就走到了第一个if,这时判断是不为空的所以都没有竞争synchronize中的资源就直接返回了,但是注意线程A并没有初始化完对象,所以这时就会出错。为了解决上述问题,我们可以引入volatile关键字,这个关键字是会有读屏障写屏障的,也就是由这个关键字修饰的变量,它中间的操作会额外加一层屏障来隔绝,详情可以参考这篇博客。就会禁止一些操作的重排序。
2.3静态内部类
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
} }
我在类内部直接定义一个静态内部类,在这个类需要加载的时候我直接把初始化的工作放在了静态内部类中,当有几个线程进来的时候,在class加载后被线程使用之前都是类的初始化阶段,在这个阶段JVM会获取一个锁,这个锁可以同步多个线程对一个类的初始化,然后在内部类的初始化中会进行StaticInnerClassSingleton类的初始化。可以这么理解,其实我们这个也是加了锁,不过这是JVM内部加的锁。
3.序列化与反序列化安全
下面先介绍一下饿汉式
public class HungrySingleton implements Serializable{ private final static HungrySingleton hungrySingleton; static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
} private Object readResolve(){
return hungrySingleton;
} }
饿汉式就是在类的初始化阶段就已经加载好了,就算你不用这个对象,这个对象也已经创建好,不像懒汉式要等到要用的时候才加载。这是两种模式的一个很大的区别,事实上饿汉式是线程安全的,就像懒汉式的内部类加载一样,是由JVM加的锁,但是两者都不一定是序列化安全的。
上面的饿汉式是序列化安全的,为什么?因为多加了readResolve()方法。这时候有人会问为什么要在饿汉式上多加一个这个方法。这里的源码我就不一一解析了。事实上在反序列化(从文件中读取类)的时候,底层会有一个判断。如果这个类在运行时是可序列化的,那么我就会在读取的时候创建一个新的类(反射创建),否则我就会让这个类为空。再后面又有一个判断,如果我的类这时候不为空,我就会通过反射尝试调用readResolve()方法,然后最终返回给我的ObjectInputStream流。没有的话我就返回之前创建的新对象。所以这就相当于覆盖了之前读取时候创建的类
4.防止反射攻击
看完上面的代码你会发现,我基本上都在私有构造器中加入一个空判断来抛出异常,反射攻击的时候,上面的懒汉式中的内部类代码和饿汉式中的序列化安全代码都是可以防御发射攻击的,当然会抛出相应异常,接下来我们介绍一下枚举单例模式
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
} }
枚举对象不能被反射创建,并且序列化与反序列化中枚举类型不会被创建出新的,下面看看枚举类型的构造器
protected Enum(String name,int ordinal){
this.name=name;
this.ordinal=ordinal;
}
可见这个构造器是有参的,并且由这两个值确定了枚举唯一性,不会由序列化与反序列化破坏。并且也是线程安全的,原理同内部类。所以非常推荐枚举类型来完成单例模式。
5.源码解析:
JDK中Runtime类就是一个单例模式,它不准外部创建实例,构造器代码如下:
/** Don't let anyone else instantiate this class */
private Runtime() {}
并且还是饿汉式,代码如下:
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
相信理解了上面的模式,可以很容易的明白这个类的设计模式
当然还有我们常用的Spring框架,简单说一下就是Spring中对象创建在Bean作用域中仅创建一个,和我们上面讲的单例还是有稍许区别,这个单例的作用域是整个应用的上下文,通俗一点理解就是Spring就像一个商店,里面的商品一种只有一个,大家看见的一个商品都是同一个,这一种商品中不会再有另一个商品了。
结合JDK源码看设计模式——单例模式的更多相关文章
- 结合JDK源码看设计模式——桥接模式
前言: 在我们还没学习框架之前,肯定都学过JDBC.百度百科对JDBC是这样介绍的[JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Jav ...
- 结合JDK源码看设计模式——原型模式
定义: 指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.不需要知道任何创建的细节,不调用构造函数适用场景: 类初始化的时候消耗较多资源 new产生的对象需要非常繁琐的过程 构造函数比较 ...
- 结合JDK源码看设计模式——简单工厂、工厂方法、抽象工厂
三种工厂模式的详解: 简单工厂模式: 适用场景:工厂类负责创建的对象较少,客户端只关心传入工厂类的参数,对于如何创建对象的逻辑不关心 缺点:如果要新加产品,就需要修改工厂类的判断逻辑,违背软件设计中的 ...
- 结合JDK源码看设计模式——模板方法模式
前言: 相信很多人都听过一个问题:把大象关进冰箱门,需要几步? 第一,把冰箱门打开:第二,把大象放进去:第三,把冰箱门关上.我们可以看见,这个问题的答案回答的很有步骤.接下来我们介绍一种设计模式--模 ...
- 结合JDK源码看设计模式——适配器模式
定义: 将一个类的接口转换成客户期望的另外一个接口(重点理解适配的这两个字),使得接口不兼容的类可以一起工作适用场景: 已经存在的类,它的方法和需求不匹配的时候 在软件维护阶段考虑的设计模式 详解 首 ...
- 结合JDK源码看设计模式——迭代器模式
前言: Iterator翻译过来就是迭代器的意思.在前面的工厂模式中就介绍过了iterator,不过当时介绍的是方法,现在从Iterator接口的设计来看,似乎又是一种设计模式,下面我们就来讲讲迭代器 ...
- 结合JDK源码看设计模式——享元模式
前言 在说享元模式之前,你一定见到过这样的面试题 public class Test { public static void main(String[] args) { Integer a=Inte ...
- 结合JDK源码看设计模式——建造者模式
概念: 将一个复杂对象的构建与它的表示分离.使得同样构建过程可以创建不同表示适用场景: 一个对象有很多属性的情况下 想把复杂的对象创建和使用分离 优点: 封装性好,扩展性好 详解: 工厂模式注重把这个 ...
- 结合JDK源码看设计模式——观察者模式
前言: 现在我们生活中已经离不开微信,QQ等交流软件,这对于我们来说不仅是交流,更有在朋友圈中或空间中进行分享自己的生活,同时也可以通过这个渠道知道别人的生活.我们在看朋友圈的时候其实我们扮演的就是一 ...
随机推荐
- arcEngine开发之查看属性表
这篇文章给出实现属性表功能的具体步骤,之后再对这些步骤中的代码进行分析. 环境准备 拖动TOCControl.MapControl控件到Form窗体上,然后拖动ContextMenuStrip控件至T ...
- mybatis自定义代码生成器(Generator)——自动生成model&dao代码
花了两天的时间研究了下mybatis的generator大体了解了其生成原理以及实现过程.感觉generator做的非常不错,给开发者也留足了空间.看完之后在generator的基础上实现了自定义的生 ...
- CMD命令锦集
虽然随着计算机产业的发展,Windows 操作系统的应用越来越广泛,DOS 面临着被淘汰的命运,但是因为它运行安全.稳定,有的用户还在使用,所以一般Windows 的各种版本都与其兼容,用户可以在Wi ...
- nodejs-2.httpfuwu
一.使用nodejs的http服务:处理 "请求或响应" 数据 要使用 HTTP 服务器与客户端,需要 require('http'). Node.js 中的 HTTP 接口被设计 ...
- javascript知识详解之8张思维导图
学习的道路就是要不断的总结归纳,好记性不如烂笔头,so,下面将po出8张javascript相关的思维导图. 思维导图小tips:思维导图又叫心智图,是表达发射性思维的有效的图形思维工具 ,它简单却又 ...
- ORA-12737: Instant Client Light: unsupported server character set CHS16GBK
当使用Navicat Premiun 英文版连接oracl时可能会报ORA-12737: Instant Client Light: unsupported server character set ...
- Android 带你玩转实现游戏2048 其实2048只是个普通的控件
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40020137,本文出自:[张鸿洋的博客] 1.概述 博主本想踏入游戏开放行业,无 ...
- 关于Flask-Login中session失效时间的处理
最近需要使用Python开发web系统,主要用到的框架就是Flask,前端使用Jinja2模板引擎和Bootstrap,web容器使用Cherrypy,其中关于Login管理的使用了Flask-Log ...
- C++中常用到的容器
这里主要讲C++中经常用到的一些保存数据的容器,其中也会介绍string. 在C++11中提到了很多容器,这里主要介绍:vector.list.map.还有一些其他的容器就不做介绍了. 1.Strin ...
- BZOJ_1797_[Ahoi2009]Mincut 最小割_最小割+tarjan
BZOJ_1797_[Ahoi2009]Mincut 最小割_最小割+tarjan Description A,B两个国家正在交战,其中A国的物资运输网中有N个中转站,M条单向道路.设其中第i (1≤ ...