前言
学习设计模式也是在代码上精进的必经之路,在阅读各种框架源码时,如果不懂设计模式,就会看得很吃力;在面试时,面试官也总会问:“了解过设计模式吗?”,如果这个时候你能和面试官多聊上几句你对设计模式的理解,是非常加分的
本系列以Java为编程语言,从设计模式中最简单的单例模式开始,介绍常用的设计模式
源码
看完有收获别忘了点个star哦~
设计模式理解与实现 - 专栏
设计模式理解与实现(1)—— 单例模式
导航
- 前言
- 源码
- 设计模式理解与实现 - 专栏
- 单例模式
-
- 作用
- 优点
- 缺点
- 实现
-
- 饿汉式
-
- 1. 最常规的饿汉式思路
- 2. 饿汉式 with 枚举
- 懒汉式
-
- 懒汉式的线程安全问题
- 线程安全的懒汉式
-
- 1. 懒汉式 with 同步方法
- 2. 懒汉式 with DCL - 双重检验锁
- 3. 懒汉式 with 私有静态内部类
单例模式
我们知道,在Java中,每一个对象都可以称为一个实例,如:
// 创建一个学生实例 stu1
Student stu1 = new Student();
在普通的类中,其构造方法常是public
的,在程序编写过程中需要用到一个类时,只需要new 类名(参数);
即可
而单例模式,顾名思义,单例,就是单一实例的意思,指在一个程序运行时,一个类只能有一个实例
作用
一个类只能有一个实例,用处都有啥呢?
- 可作为全局唯一的访问点,用于:
- 计数器
- 创建连接 / 访问资源,如数据库,IO操作等
- 要求生成全局唯一序列号的场景
- 减少一个全局类不必要的创建和销毁造成的开销
优点
说完用处说好处:
- 由于单例模式一个类只能有一个实例,减少了内存的使用,也减少了创建(特别是有些对象的产生需要吃很多资源,如读配置、创建依赖对象)和销毁实例带来的开销
- 可以避免对资源文件的多重占用,比如说调用一个单例的实例来写文件,其他线程想要执行此操作就需要等待,而不会冲突
缺点
说完好处说坏处:
- 单例模式一般是没有接口的,扩展很困难,要变化的话只能改代码:因为接口对单例没有意义,单例要求“自行实例化”,而接口、抽象类是不能被实例化的
- 单例模式和单一职责原则是冲突的:一个类应该只做自己职责内的事,而不应该关心自己是否是单例的
实现
说完概念说实现,单例模式常见的实现有饿汉式和懒汉式
*无论是哪种单例模式,其构造方法都是private
的,在他人需要获取该类的单例时,只需调用类方法getInstance()
即可
饿汉式
饿汉式单例模式将自身的类实例初始化到类私有静态常量
1. 最常规的饿汉式思路
最常规的饿汉式就像下面的代码,平平无奇
public class HungryMan {
private static final HungryMan instance = new HungryMan();
private HungryMan() { };
public static HungryMan getInstance() {
return instance;
}
}
2. 饿汉式 with 枚举
我们应知道,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,根据这个特性,我们也可以利用枚举来创建单例模式
用枚举来创建单例模式,优点有:
- 实现超简单
- 不怕反射和反序列化的破坏
public enum HungryManWithEnum {
INSTANCE;
public void doSomething(){
System.out.println("借助枚举实现饿汉式单例 - 被调用啦~");
}
}
我们只定义一个枚举变量INSTANCE
,那么INSTANCE
就已经是这个类的唯一实例了,在其它类的方法中,我们可以直接使用类名.INSTANCE.枚举实例方法
来调用单例方法,非常简单直观
懒汉式
在饿汉式中,其将其自身的类实例直接初始化到类私有静态常量,在类被创建的时候,实例也会一起被创建
但有时候类被创建时,我们还没到使用它的实例的时候,这时生成的实例就是没有用的,创建实例造成了开销,也浪费了内存空间
有没有办法让程序在调用getInstance()
方法时才生成被调用类的实例呢?有
public class LazyManWithoutDCL {
private static LazyManWithoutDCL instance = null;
private LazyManWithoutDCL(){ };
public static LazyManWithoutDCL getInstance(){
if(instance == null){
instance = new LazyManWithoutDCL();
}
return instance;
}
}
我们将实例的生成放到getInstance()
方法中,这样,就能在调用getInstance()
方法时,才生成实例了,解决了饿汉式的问题
懒汉式的线程安全问题
刚实现完懒加载,问题紧接着就来了,像上面的代码,在串行运行的时候是没有问题的,但是,在并发的情况下呢?我们来做一个测试
public class Tester {
private static int testCount = 20;
private static CountDownLatch cdl = new CountDownLatch(testCount);
public static void LazyManWithoutDCLTester() throws InterruptedException {
System.out.println("懒汉式不带DCL测试开始");
// 开启20个线程,获取懒汉式不带DCL的实例并打印其地址
for(int i=0;i<testCount;++i){
new Thread(()->{
System.out.println(LazyManWithoutDCL.getInstance());
cdl.countDown();
}).start();
}
cdl.await();
System.out.println("懒汉式不带DCL测试结束");
}
public static void main(String[] args) throws InterruptedException {
LazyManWithoutDCLTester();
}
}
我们开启20个线程,并发地访问这个单例,来看一下输出的结果:
懒汉式不带DCL测试开始
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@a47a58
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
懒汉式不带DCL测试结束
观察输出的内存地址,竟然输出了三种不一样的,这说明这个懒汉式的单例模式竟然创建了三个实例,真的太不靠谱了
那么,有解决办法吗?
线程安全的懒汉式
1. 懒汉式 with 同步方法
不就是不同步出问题了嘛,加个
synchronized
就行了~
public static synchronized LazyManWithoutDCL getInstance(){
if(instance == null){
instance = new LazyManWithoutDCL();
}
return instance;
}
确实可以解决问题,不过synchronized
锁的粒度也太大了,这样子写的单例运行效率极低
2. 懒汉式 with DCL - 双重检验锁
锁的粒度太大,我们就缩小锁的粒度就好了~
public class LazyManWithDCL {
private volatile static LazyManWithDCL instance = null;
private LazyManWithDCL() { };
public static LazyManWithDCL getInstance(){
// 第一重校验
if(instance == null){
synchronized (LazyManWithDCL.class){
// 拿到锁后,进行第二重校验,避免第二个进来的线程再次生成实例
if(instance == null){
instance = new LazyManWithDCL();
}
}
}
return instance;
}
}
需要注意的点:
- 如果没有双重检验,只有一次检验,那么有可能:第一个线程拿到锁后令
instance = new Instance();
,在缓存回写的过程中,第二个线程拿到锁了,又执行一遍instance = new Instance();
…这样在内存中就不是单例了 - 如果静态变量
instance
没有使用volatile
修饰,则第一个线程拿到锁执行完instance = new Instance();
后没有进行缓存回写,第二个线程不知道第一个线程已经给instance
赋值了,看到的还是instance == null
,就会继续执行instance = new Instance();
3. 懒汉式 with 私有静态内部类
我们知道,一个类在被加载时,才会将类里的内容进行初始化
我们在单例模式类里面再创建一个私有的静态内部类,这个内部类只能被它的外部类所创建,且只能创建一次(线程安全)
而在外部调用单例模式类的getInstance()
时,由getInstance()
去返回存在内部类里的单例,这样,在第一次调用getInstance()
时,内部类才会被加载(懒加载),并创建单例
public class LazyManWithStaticInnerClass {
private static class MyInnerClass{
public static final LazyManWithStaticInnerClass instance = new LazyManWithStaticInnerClass();
}
private LazyManWithStaticInnerClass(){ };
public static LazyManWithStaticInnerClass getInstance(){
// 直接调用静态内部类的实例
return MyInnerClass.instance;
}
}
这种方式实现的懒汉式就好像饿汉式+嵌套一样,实现起来比较简单
以上就是单例模式的内容和实现啦,有错误的地方欢迎在评论区指正哦~