结合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等交流软件,这对于我们来说不仅是交流,更有在朋友圈中或空间中进行分享自己的生活,同时也可以通过这个渠道知道别人的生活.我们在看朋友圈的时候其实我们扮演的就是一 ...
随机推荐
- CentOS 6 安装HBase集群教程
hbase0.99.2安装包下载(链接:https://pan.baidu.com/s/1dR-HB3P6mzsXVW6sLI8uxQ 密码:4g1n) 首先需要安装 zookeeper(点击查看) ...
- 学习Layui 第一天
Layui 官网说这是款经典模块化前端框架 个人觉得Layui很好用,容易上手. 在学习Layui的之前.先去官网下载必要的文件 将这些文件放入项目当中 然后可以到官网看一下示例. 可以做一个简单的表 ...
- Centos6.5DRBD加载失败,系统更换yum源(国内163)
我安装的系统是centos6.5的,要在系统上安装DRBD镜像软件,安装完后,无法加载modprobe drbd. 需要更新kernel. 1,首先,先把yum源更换成国内的,不然无法更新kernel ...
- CSS position(定位)属性
关于CSS position,来自MDN的描述: CSS position属性用于指定一个元素在文档中的定位方式.top.right.bottom.left 属性则决定了该元素的最终位置. 然后来看看 ...
- PAT1112:Stucked Keyboard
1112. Stucked Keyboard (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue On a ...
- CF泛做
CF Rd478 Div2 A Aramic script 题意:给定几个字符串,去重后,求种类 思路:直接map乱搞 #include<bits/stdc++.h> using name ...
- Vue在MVC中的进行前后端的交互
Vue在MVC中的进行前后端的交互 Preface: 由于最近在研究前端相关的技术,作为前端非常优秀的框架Vue,个人在学习的过程中遇到一些问题,网上相关资料有限,所以在这这里总结一下个人使用Vue的 ...
- Java 学习笔记 (一) If 语句
接触到的教程中if语句所包含boolean表达式都是以数字作为举例,所以误以为string类型的值也可以这样判断,结果发现下面的脚本不工作: String sysTitle = driver.getT ...
- plugin.go 源码阅读
, nil) } if c.client != nil { c.client.Close() } ...
- backend_queue.go
package nsqd // BackendQueue represents the behavior for the secondary message // storage system typ ...