前言
在介绍单例设计模式之前,先简单介绍一下java的内存分析,可以帮助我们更好的了解相关内容。在此之前,先简单介绍几个相关概念
Stack(栈)
存放基本数据类型的数据和对象的引用及存放变量,如果存放的是基本数据类型(非static),直接将变量名和值存入stack中,如果是引用,只将变量名存入栈,然后指向它new的对象(存放在堆中)
Heap(堆)
存放new产生的数据
静态域
存放在对象中用static定义的静态成员(基本类型)
常量池
存放所有数据
案例
下面将模拟几个实例先说明在证明。(此外注意String不是基本数据类型)
上面给出了一个实例,意在了解内存分配,接下来将以String为例说明
|
|
对应的测试内存地址是否相等
|
|
测试结果 true true true
说明String 是引用类型,而且上面方法产生的变量都只在栈中并且都是同一个地址
|
|
测试结果:false false
说明:new 之后的对象地址存放在堆中,同时堆中每个new之后的对象地址都是不一样的
|
|
测试结果:true false false false
说明:String类型的静态变量存放在栈中,而new之后的静态变量存放在堆中
关于内存分配,比较复杂,这里只简单验证其中一些,其他可以自己设计案例模拟
单例
顾名思义就是指唯一的对象实例
具体点说就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例只存在一个,
通常其结构如下所示。
入门案例:首先是单例类
|
|
测试用junit4进行测试
|
|
测试结果:start test.. true end test..
说明,obj1与obj2是同一个实例,间接说明了单例的特性
至此我们对单例有了一个初步的形式上的认识了,那么它的特点是什么了?主要为三点
一:单例类确保自己只有一个实例(构造函数私有:不被外部实例化,也不被继承)
二:单例类必须自己创建自己的实例
三:单例类必须为其他对象提供唯一的实例
说到单例就必然涉及到多线程,不多说,先模拟一个多线程来说明单例中存在的并发问题。
友情提醒一下,上面用的是饿汉式创建单例,就一开始就创建了一个实例,显然浪费内存,接下来将用懒汉式创建单例
主体类代码:
|
|
下面将用三个线程模拟,两个线程调用单例类,第三个线程启动前两个线程,测试用junit
|
|
简单说明一下,这两个线程目的是调用单例类(注意如果是单例,决不允许出现不同实例)
先贴出测试类:
|
|
这里我们通过控制第三个线程来构造并发,(这里采用数学中的极限思想)
|
|
测试结果:0 null null
多次运行,又出现另一种结果:6
com.zwl.pojo.Singleton@8fdcd1
null和其他不同步结果
说明在很短的时间间隔类,一个居然已经有了实例,而另一个居然是空,显然不是单例,想想倘若在此时去操作该单例,可能就会是静态类发生变化,也就是并发问题了。
很自然。那么怎么去解决了,一种很通用的方法就是加锁实现同步
双锁机制
|
|
Synchronized关键字,声明使该线程在某处同步,也就是一个线程先占用,另一个线程被阻塞,显然如果不为空,就直接返回对象,对应第二个线程也就不执行if语句了,达到单例效果,可以想象如果为计数变量,同样可以通过二次加锁实现同步
但是实际还是出现不同步,原因是jvm的一个bug,它允许无序写入线程,导致不同步,当然我们可以通过申明volatile关键字。
为什么就可以解决双重解锁的bug了?
原因是volatile保证原子性,顺序性,即在双重加锁前保证多线程实现顺序性,举个例子吧,现在有两个线程t1和t2,分别执行b,c操作,如果乱序写入,可能出现t1正在操作b还没完成,t2操作c,致使单例失败,所以需要保证顺序性。
经过不断的测试,发现完全同步,只不过时间间隔为0,和null,null,至少是同步了~
接下来简单比较一下饿汉式和懒汉式的区别
从速度和反应时间上,饿汉式加载好,从资源利用率上懒汉式好,
饿汉式很明显不会出现多线程并发问题,所以是线程安全的,而懒汉式则是非线程安全的
关于单例模式的应用,笔者在当初学习过程中,遇到很多,学习总是一个不断压缩与重新认识的过程,像sql工具类,hibernate的懒加载,mybatis及 spring中的一些配置等等,往往把握不好,就会出现错误,这里只列举一些工具类,因为它是笔者接触的第一个单例应用。
|
|