目录
一、什么是单例模式?
二、单例模式的应用场景
三、两种典型的方式实现单例模式
- 1.饿汉模式
- 2.懒汉模式
- 3.理解懒汉模式和饿汉模式
四、单例模式和线程的关系
- 1.饿汉模式是否线程安全?
- 2.懒汉模式线程安全吗?为什么?
- 2.1 如何改进懒汉模式?让代码变得线程安全呢?
————————————————
一、什么是单例模式?
单例模式是一种常见的“设计模式”
二、单例模式的应用场景
某个类,不应该有多个实例,此时就可以使用单例模式(DataSource就是一个典型的案例,一一个程序中只有一个实例,不应该实例化多个DataSource对象)。如果尝试创建多个实例,编译期就会报错。
三、两种典型的方式实现单例模式
1.饿汉模式
public class singlePattern { //先创建一个表示单例的类 //我们就要求Singleton这个类只能有一个实例 //饿汉模式的单例实现 //饿汉模式的单例实现,“饿”指得是,只要类被加载,实例就会立刻创建(实例创建时机比较早) static class Singleton{ //把 构造方法 变为私有,此时在该类外部,就无法 new 这个类的实例了 private Singleton(){ } //再来创建一个 static 的成员,表示Singleton 类唯一的实例 //static 和 类相关,和实例无关,类在内存中只有一份,static 成员也就只有一份 static Singleton instance = new Singleton(); //new没报错是因为Singleton类是singlePattern的内部类,singlePattern是可以访问内部类的private成员的 public static Singleton getInstance(){ return instance; } public static void main(String[] args) { //此处得 getInstance 就是获取实例得唯一方式,不应该使用其他方式创建实例了 Singleton s = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s == s2); } } }
只要类被加载,就会立刻实例化Singleton实例,后续无论怎么操作,只要严格使用getInstance,就不会出现其他实例。
2.懒汉模式
public class lazyPattern { //使用懒汉模式来实现,Singleton类被加载的时候,不会立刻实例化 //等到第一次使用这个实例的时候,再实例化 static class Singleton{ private static Singleton instance = null; public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } } public static void main(String[] args) { } }
类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化。
如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”
一般认为“懒汉模式” 比 “饿汉模式”效率更高。
懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销。
3.理解懒汉模式和饿汉模式
各种编辑器,有两种主要的打开方式:
- 记事本,会尝试把整个文件的内容都读取到内存中,然后再展示给用户(饿汉模式)
- vscode/sublime,只是会把当前这个屏幕内的内容(以及周围的一小点内容)加载到内存中,随着翻页,会继续加载
四、单例模式和线程的关系
1.饿汉模式是否线程安全?
安全。
类加载只有一次机会,不可能并发执行,对于饿汉模式来说,多线程同时调用getInstance,由于getInstance里只做了一件事:读取instance实例的地址=》多个线程在同时读取同一个变量,不会产生线程不安全。
2.懒汉模式线程安全吗?为什么?
懒汉模式是线程不安全的,只有在实例化之前调用,存在线程不安全问题
如果要是已经把实例创建好了~后面再去并发调用getInstance 就是线程安全的了
以上面的懒汉模式代码为例,多线程并发执行的时间线如下。
2.1 如何改进懒汉模式?让代码变得线程安全呢?
1.加锁
public class lazyPatternToSafe { static class Singleton{ private static Singleton instance = null; //方法一: public static Singleton getInstance1(){ synchronized (lazyPattern.Singleton.class){ if(instance == null){ instance = new Singleton(); } } return instance; } //方法二: synchronized public static Singleton getInstance2(){ if(instance == null){ instance = new Singleton(); } return instance; } } }
上面的“粒度”较小,下面的“粒度”较大。
加锁之后的时间线:
但是加锁之后,仍然存在效率问题。上面的改进代码,哪怕实例已经创建好了,但是每次调用getInstance还是涉及加锁解锁,而这里的加锁解锁已经不必要了。只要代码中涉及到锁,基本上就和高性能无缘了(因为涉及到锁之间的等待)
2.解决方案:只有在实例化之前调用的时候加锁,后面不加锁~
static class Singleton { private static Singleton instance = null; public static Singleton getInstance1() { if(instance == null){ synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
上述双线程执行的时间线如下:
此时即能保证线程安全,也能保证程序运行的效率。
3.但是还是存在问题,此处的多个操作,可能会被编译器优化,只有第一次读才从内存中读取,后续的读就直接从CPU中读取寄存器。就可能导致线程1修改之后,线程2没有读到最新的值,内存可见性导致的线程不安全问题(先加锁的线程在修改,后加锁的线程在读取)。
最终版本:加volatile关键字
static class Singleton { //为了解决内存不可见问题,需要加上关键字volatile private volatile static Singleton instance = null; public static Singleton getInstance1() { if(instance == null){ synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
上面的代码:为了保证线程安全,涉及到三个要点:
1.加锁 【保证线程安全】
2.双重if 【保证效率】
3.volatile【避免内存可见性引来的问题】