@TOC
单例模式
什么是单例模式
要求我们代码中的某个类,只能有一个实例,不能有多个实例。实例就是对象。就是说某个类只能new 一个对象,不能new多个对象。
这种单例模式,在实际开发中是非常常见的,也是非常有用的。开发中的很多“概念”,天然就是单例的。
比如:
使用JDBC操作数据库,此时数据库连接可以通过数据库连接池数据库连来获取
DataSource ds=new MySqlDataSource(); Connection c=ds.getConnection() ;//获取数据库连接连接池就可以使用单例模式,创建唯一的一个对象.
大部分跟数据有关的东西,服务器里面只存一份。那么,就都可以使用“单例模式”来进行表示。
单例模式的两种经典实现
单例模式中有两个典型实现:
我们来通过一个生活上的例子来给大家讲讲什么是饿汉模式,什么是懒汉模式。
洗碗:
第一种情况:假设我们中午吃饭的时候,一家人用了4个碗。然后吃完之后,马上就把碗给洗了这种情况,就是饿汉模式
但是在计算机中,普遍认为 懒汉模式 比 饿汉模式好。主要因为 懒汉模式 的效率更高
也很好理解:洗 2 个 碗,肯定比洗4个碗轻松。
所以用几个洗几个。根据需要,进行操作。
“懒” 这个字一般 在计算机中,是一个褒义词。
1. 饿汉模式 (线程安全)
分析:
(1) 为什么是线程安全的?
由于在类加载的时候就初始化了(第2行),只有一份,所以是线程安全的.
(2) 缺点
①还没有使用,就浪费了内存空间.
②new对象(没有执行类加载,会先执行,再执行成员变量+实例代码块+构造方法),可能抛异常,也就是还没有调用getInstance,在类加载时就抛出了异常,那么以后调用getInstance方法时,都是会出现问题的.
2. 懒汉模式 (线程不安全)
由于比较懒,你让我干活,我才开始,否则我不会主动干的~
说人话就是: 类加载时,不急着初始化对象,第一次调用,初始化对象,后边再次调用,直接返回第一次创建好的对象.
//单例模式 - 懒汉模式 class Singleton2{ //1、现在就不是立即初始化实例 private static Singleton2 instance;// 默认值:Null //2、把构造方法设为 private private Singleton2(){}; //3、提供一个公开的方法,来获取这个 单例模式的唯一实例 public static Singleton2 getInstance(){ // 只有当我们真正用到这个实例的时候,才会真正去创建这个实例 if(instance == null){ instance = new Singleton2(); } return instance; } } public class Test20 { public static void main(String[] args) { Singleton2 instance = Singleton2.getInstance(); } }分析:
为什么是线程不安全的?
在getInstance()方法中,总共有三行Java代码。
而且其中instance = new Singleton() 会被分解成三行字节码指令,相当于并发并行的对多行共享变量的操作,所以是线程不安全的.
区别:
饿汉模式 和 懒汉模式 的唯一区别:
在于 创建实例的时机不一样。
饿汉模式 是 类加载时,创建。懒汉模式 是 首次使用时,创建。
所以懒汉模式就更懒一些,不用的时候,不创建;等到用用的时候,再去创建。这样做的目的,就是节省资源。
其实在计算机很多其它场景中,也会涉及这情况。
一个典型的案例:
notepad 这样的程序(记事本软件),在打开大文件的时候是很慢的。假如,你要打开一个 1G 大小的文件,此时 notepad 就会尝试把这 1 G 的 所有内容都读到内存中。将 1G 的数据量 存入 内存,显然是非常慢的。不管你要不要,全部都给你。这就是 饿汉模式。
问题也随之而来:这些数据,我们真的能全部用得到吗?显示是不太可能的。因此就会浪费很多资源。 像一些其他的程序,在打开大文件的时候就有优化。假设也是打开 1G的文件,但是只先加载这一个屏幕中能显示出来的部分。看到哪,加载到哪里。这样不会用空间上的浪费这就是 懒汉模式
实现线程安全的单例模式
1. 懒汉模式+synchronized静态同步方法 (线程安全但效率差)
说到让一个代码线程安全,我们自然而然的就想到加锁!但是问题就在于:在哪个地方加锁合适呢?其实也很好观察,将 if 语句的执行操作 给 加锁,使其两个操作为原子性。直白来说: 就是 if 语句 打包成“一个整体”,就跟前面分析 count++ 一样。一致性执行完。
加锁范围 一定要包含 if 语句!!!要不然没有效果,就像下面这样!
本来我们是想将 读 和 写 操作,打包成一个整体,但是现在只是 针对写操作进行加锁,这时候就跟没加锁 一样,是没有区别的。
请大家注意!并不是代码中有 synchronized,一定就是线程安全的。这需要看 synchronized 加的位置,也要正确。所以 synchronized 写的位置。不能随便。 回过头来,我们再来看一下 synchronized 锁的对象写我们应该些什么。
//单例模式 - 懒汉模式 class Singleton2{ //1、就不是立即初始化实例 private static volatile Singleton2 instance;// 默认值:Null //2、把构造方法设为 private private Singleton2(){}; //3、提供一个公开的方法,来获取这个 单例模式的唯一实例 public static Singleton2 getInstance(){ // 只有当我们真正用到这个实例的时候,才会真正去创建这个实例 synchronized(Singleton2.class){ if(instance == null){ instance = new Singleton2(); } } return instance; } }虽然我们确实通过上述加锁操作,解决了 if 语句 的原子性问题。
但是!这样的程序,还存在这几个问题!
1.代码执行效率问题
2、指令重排序虽然其他线程再调用 单例线程的时候,也是加了 synchronized 的。减缓了循环速度,从而保证了 内存可见性。但是!还有一个问题,来看下面。
此时,我们才完成一个线程安全的单例模式 - 懒汉模式1、正确的位置加锁2、双重if判定3、volatile关键字
2. 懒汉模式+二次判断(双重校验锁,线程安全且效率高)
用法:
- 双重校验: 两个if.
- 锁: synchronized
- 注意使用静态变量(引用) 使用volatile.
分析:
- ①对象初始化前,多线程调用getInstance(),需要保证线程安全,即执行第5,6,7,8行.
- ②对象初始化以后,只执行 if 判断和 return 语句,即只执行第5,9行.
② 明显比 ① 执行的情况多很多,所以考虑不加锁,提高效率.
② 不需要加锁,可以使用volatile保证可见性.
即:
第5行: 初始化完成之后,不需要加锁,使用volatile修饰变量,保证可见性,能满足线程安全,代码行本身就是原子性. 可以并行并发的执行,提高了效率.
第6行: 没有初始化完成时,创建对象需要加锁来保证线程安全.
第7行: 竞争锁失败的线程,还会执行同步代码块,需要再次判断,保证只初始化一次.
第9行: 引用使用了volatile关键字,还有建立内存屏障,禁止指令重排序的功能(new 分解的三条指令: 分配内存空间,初始化对象,赋值给变量)