单例模式
一些概念
单例模式是一种设计模式.
设计模式类似于"棋谱",即固定套路,专门针对一些特定场景,给出的一些比较好的解决方案.
设计模式,就是针对编写代码过程中的"软性约束"(可以遵守,也可以不遵守).
框架,就是针对编写代码过程中的"硬性约束". 针对一些特定的场景问题,大佬们把基本的代码,已经写出来了,大部分的逻辑也都写好了,留出来一些空位,让你在空位上填充一些自定制的逻辑.
在开发中希望有的类在一个进程中,不应该存在多个实例(对象).
此时,就可以使用单例模式,限制某个类,只能有唯一实例.
比如一般来说,一个程序中,只有一个数据库,对应的mysql服务器只有一份,此时DataSource这个类,就没有必要创建出多个实例.
DataSource dataSource = new MysqlDataSource();
此时就可以使用单例模式,来描述DataSource,这样就可以避免不小心创建出多个实例了~
Java 中单例模式的实现有很多种写法,(不乏有一些,奇葩的做法)只介绍两种最主流的写法
饿汉模式
代码:
// 创建一个单例的类
// 饿汉方式实现
class Singleton {
// 在类被加载的时候,就会创建出这个单例的实例
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
public class Demo15 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
运行结果:
此时只要不在其他代码中,new Singleton这个类,每次使用时都通过getInstance来获取,这样的话这个类就是单例的了~
但是万一别人一不小心重新new了对象,那不就完了?
是的,所以我们也要解决这个问题~
把构造方法用private 修饰可以啦~
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 单例模式最关键的部分
private Singleton() { }
}
private Singleton() { }
这意味着在类的外面,就无法调用构造方法,也就无法创建实例了~
有人可能就要说了: 你不是private,那我通过反射拿到构造方法,通过反射api来调用,不就能new出实例了吗?
当然是可以的(大力出奇迹)…
单例模式,只能避免别人"失误",不能应对别人的"故意攻击".
(虽然是有办法规避故意攻击的,但是代价太大了)
懒汉模式
懒汉模式: 推迟了创建实例的时机,第一次使用的时候,才会创建实例.
能不搞就不搞,很多时候,就可以剩下一部分开销.
比如,有一个编辑器,打开一个非常大(1G)文本文档
- 一启动,就把所有的文本内容都读取到内存中,然后再显示到界面上[饿汉]
- 启动之后,只加载一小部分数据(一个屏幕能展示的最大数据),随着用户进行翻页操作,再按需加载剩下的内容.[懒汉]
懒汉模式代码:
// 懒汉模式实现的单例模式
class SingletonLazy {
// 此处先把这个实例的引用设为null,先不急着创建实例
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() { }
}
class Demo16 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
可以看到确实是单例模式.
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
当首次调用getInstance时,因为现在引用为null,就会进入if分支,从而创建实例,后续再重复调用getInstance,都不会创建实例~
饿汉和懒汉是否是线程安全?
上述单例模式的讨论,只是"引子",接下来才是正题.
上述写的 饿汉 和 懒汉 单例模式代码,是否是线程安全的?
(如果多线程环境下,调用getInstance,是否会有问题呢?)
其实,饿汉没有问题,但是懒汉有问题~
多个线程针对一个变量进行修改,如果只是读取,则没有问题~
但下面的懒汉模式明显不是.
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
先判定,再修改. 这种代码模式,是属于典型的线程不安全代码,判断和修改之间可能涉及到线程的切换.
假设现在有两个线程,t1和t2.
画个图更容易理解:
通过上图,我们可以看出,之所以出现线程安全问题,是因为在if判定和new操作之间出现了线程切换.
如果要解决上述问题.很简单,只要使用锁把if和new包裹到一起,变成一个"原子"就行啦~
初步改进
代码如下:
class SingletonLazy {
// 此处先把这个实例的引用设为null,先不急着创建实例
private static SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance() {
//使用锁,把if和new操作包裹到一起
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy() { }
}
仔细想想.加锁之后,确实解决了线程安全问题,但是加锁同样也可能带来阻塞,影响到性能.
如果上述代码,已经new完对象了,if分支再也进不去了,后续的代码都是单纯的读操作.
此时,getInstance不加锁,也是线程安全的.那就没有必要加锁了~
因此,针对这个问题,还需要进一步的改进.
进一步改进
通过条件判断,在应该加锁的时候才加锁,不需要加锁的时候,直接跳过加锁.
class SingletonLazy {
// 此处先把这个实例的引用设为null,先不急着创建实例
private static SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance() {
// 不需要加锁,就跳过
if(instance == null) {
//使用锁,把if和new操作包裹到一起
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
这样就可以解决线程安全问题了…吗?
再次改进
instance = new SingletonLazy();
上述代码可能会因为指令重排序(一种编译器的优化方式),从而引发线程安全问题.
这行代码,看似只有一句,其实并不然,它干了三件事:
- 分配内存空间
- 执行构造方法
- 将内存空间的地址赋值给引用变量
编译器可能按照 1 2 3 的顺序来执行
也可能按照 1 3 2 的顺序来执行
对于单线程来说,先执行2,还是先执行3,本质上是一样的.
但是在多线程环境下,如果按照 1 3 2 的顺序来执行,那就可能会出现问题.
一图胜千言:
有没有办法来解决呢?
当然有啦,我们发现出现上述问题的原因是编译器优化,那我们让编译器不要优化这里就行了.
也就是使用"volatile"关键字.
把代码改成:
private static volatile SingletonLazy instance = null;
此时编译器就知道了,instance 是易失的,那么编译器围绕这个变量的优化就会非常克制.(不仅仅会在读取变量的优化上克制,也会在修改变量的优化上克制)
因此Java的volatile
有两个功能
- 保证内存可见性
- 禁止指令重排序(针对赋值)
最终代码:
// 懒汉模式实现的单例模式
class SingletonLazy {
// 此处先把这个实例的引用设为null,先不急着创建实例
private static volatile SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance() {
if(instance == null) {
//使用锁,把if和new操作包裹到一起
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
总结
设计模式,就是针对编写代码过程中的"软性约束"
框架,就是针对编写代码过程中的"硬性约束".
在开发中希望有的类在一个进程中,不应该存在多个实例(对象).此时,就可以使用单例模式,限制某个类,只能有唯一实例.
饿汉模式(线程安全): 在类被加载的时候,就会创建出这个单例的实例.
饿汉模式代码:
// 饿汉方式实现
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 单例模式最关键的部分
private Singleton() { }
}
懒汉模式: 在程序第一次使用这个实例的时候,才会创建实例.
懒汉模式代码(线程不安全):
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
懒汉模式代码(线程安全):
// 懒汉模式实现的单例模式
public class Singleton {
// 声明,并用volatile修饰,保证在多线程环境下的有序性
private volatile static Singleton instance = null;
// 私有构造方法
private Singleton () {}
// 对外提供一个获取实例的方法,
public static Singleton getInstance() {
// 使用双重if检查, 降低锁竞争的频率
if (instance == null) {
// instance没有被实例化才去加锁
synchronized (Singleton.class) {
// 获取到锁后再检查一遍,确认instance没有被实例化后去真正的实例化
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Java的volatile
有两个功能:
- 保证内存可见性
- 禁止指令重排序(针对赋值)
本文到这里就结束啦~