把书读薄 | 《设计模式之美》设计模式与范式(创建型-单例模式)(上)

简介: 之前做组内分享写过一篇 《重学设计模式 | 单例模式(Singleton Pattern)》,部分参考了《设计模式之美》,故直接搬运,且对此进行一些内容补充,对应 设计模式与范式:创建型(41-43),单例模式是日常开发中是用得最多的模式~二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。

0x1、定义


网络异常,图片无法展示
|


0x2、单例写法的演进


① 饿汉式(没有懒加载,线程安全,常用)


public class Singleton () {
    private static Singleton instance = new Singleton();
    private Singleton(){ }
    public static Singleton getInstance() {
        return instance;
    }
}


  • 优点:类装载(ClassLoader)时就完成实例化,避免线程同步问题,没加锁,执行效率高;


  • 缺点:没有懒加载,即使没用到这个实例还是会加载;


② 懒汉式(懒加载,线程不安全,不推荐使用)


就是在饿汉式的基础上加了一个判空,调用getInstance()方法才初始化实例:


public class Singleton {
    private static Singleton instance = null;
    private Singleton() { }
    private static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}


虽然实现了懒加载,却存在线程安全问题,比如两个线程,都刚好走到判空,实例为空初始化,结果可能导致实例化了两个Singleton对象,破坏了单例,一种升级版的解决方式是加锁。


③ 升级版懒汉式(线程安全,但效率低,不推荐使用)


public class Singleton { 
    private Singleton instance = null;
    private Singleton() { }
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}


给getInstance()函数加锁,保证了线程安全,但也导致了函数的并发度很低,相当于串行操作,频繁调用此函数,会频繁地加锁、释放锁、效率太低。


而且,其实只需要在new的时候考虑线程同步就行了,所以改进后的DCL单例来了~


④ 懒汉式双重校验锁(DCL,线程安全,推荐使用)


public class Singleton {
    private static Singleton instance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}


代码看似完美,但还存在最后一个问题:指令重排序


JVM在保证最终结果正确的情况下,可以不按照编码的顺序执行语句,尽可能地提高程序的性能。


创建一个对象,在JVM中会经过这三步:


  • 1、为instance分配内存空间;
  • 2、初始化instance对象;
  • 3、将instance指向分配好的内存空间;


在这三步中,第2、3步有可能发生指令重排现象,导致对象的创建顺序变成了:1-3-2,多个线程在获取对象时,有可能获取到为初始化的instance对象对象,引起NPE异常。示例流程图如下所示:


网络异常,图片无法展示
|


而使用volatile关键字修饰变量,可以防止指令重排序(原理是内存屏障),使得指令执行顺序与程序指明顺序一致。


修改后的代码如下:


public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}


上面这个防止指令重排序又称**有序性,接着说说可见性**,即 每一时刻线程读取该变量的值都是内存中最新的值


也可以这样理解:


volatile修饰的变量,在线程对其进行写入操作时,不会把值缓存到工作内存中,而是直接将修改后的值重新刷回主内存。而当处理器监控(嗅探)到其他线程中该变量在主内存中的内存地址发生变化时,会让这些线程重新到主内存中拷贝这个变量的最新值到工作内存中,而不是继续使用工作内存中的旧缓存。


未加volatile的简单代码示例如下:


public class JavaTest {
    public static void main(String[] args) {
        Test test = new Test();
        test.start();
        while (true){
            if (test.isFlag()) {
                System.out.println("flag为true");
                break;
            }
        }
    }
}
class Test extends Thread {
    private boolean flag = false;
    public boolean isFlag() {
        return flag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + flag);
    }
}


运行后,程序只输出了一个flag = true,然后就死循环卡住了,不会输出:flag为true!原因是:


我们在子线程中修改了flag的值,但是主线程并不知道这个更改,使用的依旧是之前的旧值,所以会一直死循环。


而只要我们为flag添加volatile修饰,程序就能正常结束了:


网络异常,图片无法展示
|


除此之外为if(test.ifFlag())加上synchronized锁也可以解决可见性问题~


线程在进入synchronized代码块前后,会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存称为副本,执行代码,将修改后的副本的值刷回主内存中,最后线程释放锁。


最后一点,volatile无法保证原子性(一次操作,要么完全成功,要么完全失败),比如下面的代码示例:


public class VolatileTest {
    public static volatile int count = 0;
    public static void increase() {
        count++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for(int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        // 等待所有累加线程结束,此处>2的原因是idea执行用户代码时会创建一个监控线程Monitor
        // 可以调用 Thread.currentThread().getThreadGroup().list() 查看一番~
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }
}


创建了20个线程,每个线程对变量count进行1000此自增,并发结果正常应该是20000,但实际运行过程中结果很多时候都不够20000,原因是count++这个自增操作不是院子操作。解决方法也很简单,要么加锁,要么使用原子类,如:AtomicInteger。

总结下就是:


volatile是JVM提供的一种最轻量级的同步机制,可看做轻量版的synchronized,但不保证原子性,如果是对共享变量进行多个线程的赋值而没有其他操作,那么可以用volatile来代替synchronized。


相关文章
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
27 2
|
12天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
20天前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
15 1
|
1月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
29 1
|
26天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
25 0
|
29天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
本教程详细讲解了Kotlin中的单例模式实现,包括饿汉式、懒汉式、双重检查锁、静态内部类及枚举类等方法,适合需要深入了解Kotlin单例模式的开发者。快速学习者可参考“简洁”系列教程。
27 0
|
29天前
|
设计模式 存储 数据库连接
Python编程中的设计模式之美:单例模式的妙用与实现###
本文将深入浅出地探讨Python编程中的一种重要设计模式——单例模式。通过生动的比喻、清晰的逻辑和实用的代码示例,让读者轻松理解单例模式的核心概念、应用场景及如何在Python中高效实现。无论是初学者还是有经验的开发者,都能从中获得启发,提升对设计模式的理解和应用能力。 ###
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 安全
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和重用性的关键技术之一。本文将深入探讨单例模式(Singleton Pattern)的原理、实现方式及其在PHP中的应用,同时通过实例展示如何在具体的项目场景中有效利用单例模式来管理和组织对象,确保全局唯一性的实现和最佳实践。
|
1月前
|
设计模式 传感器 运维
Harmony设计模式-单例模式
Harmony设计模式-单例模式
59 0