单例模式是一种设计模式
第一句话,什么意思。
要知道什么是单例模式,首先要知道什么是设计模式。
之前学习过框架(比如spring这种,就是轻量级框架)
框架算是硬性的规定。
设计模式算是软性的规定。
怎么解释这个“软性的规定”呢。
就类似与,棋谱,对弈的时候,照着棋谱走,棋力就不会很差。再差也差不到哪里去。但是也不是说必须每一步都按照棋谱走,就算有一两步不是按照棋谱来走的,也不见得一定会输。
总之,框架和设计模式都是大佬设计出来的,就算是我这种小菜鸡,也能根据设计模式写出不错的代码~~~。
这里补充一个误区,很多人都以为设计模式有23种,实际上,设计模式有很多种,不只有23种!!!
有3个大佬,合伙写了一本书,介绍了23种常用的设计模式~~~
这本书很火~~~导致有的人误以为,设计模式就这23种!!!
设计模式有很多种,不同的语言中,也有不同的设计模式。
(设计模式也可以认为是对编程语言语法的补充)
(一般本科阶段校招,掌握两个设计模式----一个单例模式,一个工厂模式,应对校招,完全够了)。
什么是单例?
单例 = 单个实例
实例就是对象,对象就是实例(这里的对象和java面向对象说的不是同一个,指的是一个程序,一个进程,一个主机)
如果一个类,在一个进程中,只能有一个实例(原则上不应该有多个)
使用单例模式,就能对咱们得代码进行一个更严格的校验和检查
有的代码需要使用一个对象,来管理/持有大量的数据,此时有一个对象就可以了。
比如,一个对象管理了10个G的数据。如果你不小心创建出多个对象,内存空间就会成倍增长
那么
唯一的对象如何保证?
当然可以通过“君子约定”的方式,写一个文档,文档上约定,每个接手代码的程序员,都不能把这个类创建多个实例~~~
(但是很明显,这个约定不靠谱,人是靠不住的,人总会出差错)
还是期望机器(编译器)能够对代码中指定的类,对创建的实例进行个数校验。如果发现创建多个实例了,就直接报错,编译无法通过这种。
如果能做到这样,就可以非常放心的编写代码,不必担心因为失误创建出多个实例了。
Java语法,本身没有办法直接约定某个对象最多只能创建几个实例!!!
这个时候就需要通过一些“奇技淫巧”来变相的实现这样的效果
实现单例模式的方式
实现单例模式的方式,有很多种,此处介绍两种最简单的实现方式
1. 懒汉模式
2. 饿汉模式
在软件设计中,饿汉模式和懒汉模式是两种常见的资源加载策略。
饿汉模式
这时候代码先创建一个类 Singleton
//期望这个类只能有唯一的实例(一个进程中)
class Singleton {
}
singleton这个单词源于single,single什么意思呢,什么不认识?
single dog你总认识了吧
说的就是你,屏幕前的单身狗☞
single dog是单手狗的意思,那么single自然就是单身,单个的意思了
singleton就是单独的人,独生子女,进而也有单例的意思了
先写一个引用
private static Singleton instance;私有 静态 类型为Singleton,名称是instance(意思是实例)
这个,就是我们创建出的唯一实例的引用
因为是饿汉模式
就是已经饿的迫不及待了
程序一开始执行到类的时候就等不及的创建实例了
因此写为
private static Singleton instance = new Singleton();但是要要创建实例,只能自己创建,因此构造方法也设置为私有的
private Singleton(){}但是实例instance是私有的
那使用实例的时候怎么获取到呢
那就直接使用get
写个get方法,设为public
public static Singleton getInstance(){
return instance;
}
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
饿汉模式的一个明显缺点是,它在启动时会一次性加载所有数据。例如,当我们使用编辑器打开一个非常大的文件(比如10GB),饿汉模式会将整个文件加载到内存中, 完成以后才能够进行统一的展示。这意味着用户必须等待文件完全加载后才能看到内容,这可能会导致长时间的延迟。
为了解决这一问题,懒汉模式应运而生。懒汉模式的关键在于“懒”这个字,即延迟加载。它的核心思想是只在需要的时候才加载数据。例如,编辑器在打开文件时,只会读取一小部分数据(比如10KB),并将这些内容首先展示给用户。随着用户的操作(如翻页),编辑器会继续加载后续的数据。这样不仅提高了用户体验,还节省了系统资源。
懒汉模式
老规矩,写一个实例
private static SingletonLazy instance然后置为空
private static SingletonLazy instance = null;但是和恶汉不一样的是,饿汉上来直接创建,懒汉先置为空
仍然是私有的构造方法
private SingletonLazy() {}但是获取实例的方法稍微不一样了
饿汉可以直接获取,而我们懒汉要考虑的就很多了
想要获取实例的时候,能够直接return instance吗
当然那不行,因为现在还是null呢
所以咱们得先new一个
instance = new SingletonLazy();但是每次访问都得重新创建一个吗
肯定不行
当instance不为null的时候就不new了
这个还是很简单的对吧
public static SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; }这个时候就算做,如果是首次调用getinstance,instance引用为null,就会进入if条件,那么就会把这个instance实例创建出来并且返回
如果是后续再调用getinstance,由于instance已经不再是null了
这样设计,仍然可以保证,该类的实例是唯一一个
与此同时,创建实例的时机就不是程序驱动的时候了,而是第一次调用getinstance的时候
这个操作的执行时机就不得为知了,就看自己程序的实际需求,大概率比饿汉的方式要晚一些,甚至也有可能整个程序根本就没用到这个方法,就把创建的操作也省下来了
有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步得来决定创建某个实例
就比如,肯德基有个操作是“疯狂星期四”
对于肯德基 点餐系统来说,就可以判定一下今天是星期几,
如果是星期四,才加载疯狂星期四相关的逻辑和数据
如果不是星期四,就不用加载了(节省了一定的开销)
现在写一个main方法测试一下代码
public static void main(String[] args) { SingletonLazy lazy1 = SingletonLazy.getInstance(); SingletonLazy lazy2 = SingletonLazy.getInstance(); System.out.println(lazy1 == lazy2); }创建了两个
SingletonLazy
实例lazy1
和lazy2
,如果它们是同一个实例,那么比较结果应为true
。结果显而易见,肯定是返回的true
谁是线程安全的?
现在加上多线程,看看单例模式和多线程能碰撞出什么样的火花
现在我要提出一个问题
饿汉模式和懒汉模式,谁是线程安全的?
首先,我们看看,饿汉模式,它的getinstance只有一个操作
只有一个读操作
本质上,多个线程对同一个变量进行读操作,是线程安全的!!!
现在看看懒汉模式呢:
既有读操作,也有写操作
那么多个线程对同一个变量进行写操作的时候,就不是线程安全的
比如下面这种执行顺序
(win11的画图功能咋这么糊)
此时就new出了两个实例
这就不符合单例模式的初衷了
这就不是单例模式了,这就有bug了
这就不是单例模式
单例模式的对象,可能非常大,一个对象可能要管理10个G的内存(我现在自己的电脑都才16G),要是new了两个,给我卡死了,我直接换一台新的(bushi)
于是上述讨论的过程,得出来的结论就是:
懒汉模式不是线程安全的,饿汉模式是线程安全的
怎么改进懒汉模式
怎么解决懒汉模式的线程安全问题呢?
怎么改进懒汉模式
(盲猜加锁)
可以在外面套一层锁
现在我随便把锁加在if里面
这样真的可以吗
这个锁是静态的,就算是两个线程也只有一个锁,加锁按理说成功了啊
现在要注意了啊
多线程水很深,很复杂,一点小小的变动,会导致得出来的结果完全不一样
现在咱们看看
还是不行,还是new了两个实例
这个锁白加了
(我希望我这个例子没有白写,万一面试官问到屏幕前的你的时候,为什么不在这里(if里面)加锁,你要能回答的上来)
那就把synchronize枷锁加到if外面呗
现在的执行顺序是
现在是线程安全的了嘛
线程1和线程2都只new了一个对象
问题已经迎刃而解了
但是上述代码仍然有问题,换而言之
咱们加锁的目的是什么,场景是什么
这个自己心里清楚吗
不就是因为第一次调用getinstance的时候会出现线程安全的问题吗
也就是说
只有第一次调用getinstance的时候
才需要加锁
其他时间都是不需要加锁的!!!
加锁意味着阻塞等待!
如果我们已经不是第一次调用getinstance了,已经没有线程安全了,
要是有很多进程同时调用一个没有线程安全的方法的话,就会有加锁操作,加锁操作意味着阻塞等待,时间就会浪费很多了
因此
我们再在外面判断一次instance是否为null
不为空就直接返回instance
而不是先加锁自己判断了以后再返回,这样其他线程只能阻塞等待再判断了
所以大家在加锁的时候,不要贪杯,不该加锁,就不能随便乱加,需要加锁的时候再加速
所以说除了StringBuilder还提供了StringBuffer,除了Vector还提供了ArrayList....
(StringBuffer线程安全,StringBuilders虽然线程不安全但是性能相对较高)
(Vector线程安全但是性能不如ArrayList)
最终版本:
public static SingletonLazy getInstance() { if (instance == null) { synchronized (lock) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; }
别看这有两个一样的if判断 instance == null,在单线程/非阻塞的代码中确实是无意义的,但是在多线程环境下,这样的代码十分有意义
看起来是一样的条件,实际是,这两个条件的结果可能是截然相反的
synchronized可能让当前这个线程阻塞,阻塞的过程中,可能会有别的线程修改了instance,这两个if中间隔得时间,可能是沧海桑田...
比如:
就好比二十年前你坚定不移的说着你爱我,但是二十年后答案还会始终如一吗