设计模式之单例模式|8月更文挑战

image.png

今天咱们正式进入java设计模式的学习之旅,先从单例模式开始讲起。java

你们能够查看我设计模式专栏篇的引文,关注个人设计模式专栏:java设计模式攻坚准备程序员

话很少说,进入正题算法

单例模式

  • 单例设计模式(Singleton Design Pattern)理解起来很是简单。一个类只容许建立一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫做单例设计模式,简称单例模式。数据库

  • 咱们在编程开发中常常会遇到这种场景:须要保证一个类只有一个实例,哪怕多线程同时访问,并且须要提供一个全局访问此实例的点。能够总结出一个理论,单例模式主要解决的是一个全局使用的类,被频繁地建立与销毁,从而提高代码的总体性能编程

  • 应该看到这里大概都有所了解了,晦涩的文字对于单例模式的描述也比较容易理解了。那么咱们看下经典的单例场景:设计模式

  • 如数据库的链接池不会反复建立,Spring中一个单例模式Bean的生成和使用,代码中须要设置全局的一些属性并保存。安全

这就是单例模式,那么针对上述描述咱们下面咱们用实际案例和代码来让你有更深刻的理解。markdown

7+2种单例模式实现方式

单例模式的实现方式比较多,可是惟一只有一个目的,永远只建立一个实例(对象),下面咱们先对7种常见的案例一一进行阐述,多线程

为何还有2种呢,对的 由于咱们只有一个目的,保证只建立一个实例,那么任何方式只要知足这个均可以,我会写出两种不怎么常见的,可是我的认为可使用的方式来设计单例模式。并发

一、静态类

/** * 单例静态类 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_01 {
    public static Map<String,Object> cacheInfo = new ConcurrentHashMap<>();
}


public static void main(String[] args) {
    Map<String,Object> map =  Singleton_01.cacheInfo;
}
复制代码

这种静态类方式在平常的业务开发中很常见,它能够在第一次运行时直接初始化Map类,用于全局访问,使用静态类方式更加方便

二、懒汉模式(线程不安全)

/** * 单例之懒汉模式 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_02 {

    private static Singleton_02 instance;

    private Singleton_02(){

    }
    //获取对象实例
    public static Singleton_02 getInstance(){
        if (null != instance){
            return instance;
        }
        //若是为空 则内部new对象
        instance = new Singleton_02();
        return instance;
    }
}

//获取对象
public static void main(String[] args) {
    Singleton_02 singleton_02 =  Singleton_02.getInstance();
}

复制代码

单例模式有一个特别重要的特色是不容许外部直接建立,也就是 new Singleton_02(),所以这里在默认的构造函数上添加了私有属性private。

思考:若是有多个访问者同时获取对象实例,会不会形成多个一样的实例并存。答案是确定的。 若是当第一次访问尚未建立完成实例(正在建立中),结果第二个线程请求进来了,都会走到new实例当中

三、懒汉模式(线程安全)

咱们在上述的基础之上作下处理:添加 synchronized 锁控制

/** * 单例之懒汉模式 synchronized加持 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_01 {

    private static Singleton_01 instance;

    private Singleton_01(){
    }

    //获取对象实例加 synchronized 控制,
    public static synchronized Singleton_01 getInstance(){
        if (null != instance){
            return instance;
        }
        //若是为空 则内部new对象
        instance = new Singleton_01();
        return instance;
    }
}
复制代码

synchronized加锁,便保证了每次只能有一个线程加锁成功,那便只能new一次对象。

此种模式虽然解决了线程不安全的问题,但因为把锁加到方法中后,全部的访问由于须要锁占用,致使资源浪费。除非在特殊状况下,不然不建议用此种方式实现单例模式。

四、饿汉模式(线程安全)

/** * 饿汉模式 线程安全 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_03 {

    private static Singleton_03 instance = new Singleton_03();

    private Singleton_03(){
    }

    //获取对象实例
    public static Singleton_03 getInstance(){
        return instance;
    }
}
复制代码

这种方式与开头的第一个实例化 Map 基本一致,在程序启动时直接运行加载,后续有外部须要使用时获取便可。这种方式并非懒加载,也就是说不管程序中是否用到这样的类,都会在程序启动之初进行建立。

五、类的内部类(线程安全)

/** * 单例模式 匿名内部类 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_04 {

    private static class singletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    private Singleton_04(){
    }

    //获取对象实例
    public static Singleton_04 getInstance(){
        return singletonHolder.instance;
    }
}
复制代码

使用类的静态内部类实现的单例模式,既保证了线程安全,又保证了懒汉模式,同时不会由于加锁而下降性能。

这主要是由于JVM虚拟机能够保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下能够被正确地加载。这也是推荐使用的一种单例模式。

虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其余线程都须要阻塞等待,直到活动线程执行方法完毕。

因此线程安全!!!

六、双重锁校验(线程安全)

/** * 双重锁校验(线程安全) * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_05 {

    private static volatile Singleton_05 instance;

    private Singleton_05(){

    }

    //获取对象实例
    public static Singleton_05 getInstance(){
        if (null != instance){
            return instance;
        }
        synchronized (Singleton_05.class){
            if (null == instance){
                instance = new Singleton_05();
            }
        }
        return instance;
    }
}
复制代码

这种方式是极为弥补懒汉模式的不足,无需每次都去上锁。双重锁的方式是方法级锁的优化,减小了获取实例的耗时。能够适当运用此模式,

七、枚举单例(线程安全)

/** * 单例模式之枚举 * @Date 2021/8/1 8:04 下午 * @Author yn */
public enum Singleton_06 {

    INSTANCE;

    public void testInstace(){
        System.out.println("我是单例模式之枚举 --> -->");
    }
}
复制代码

此种方式多是平时最少用到的。可是这种方式解决了最主要的线程安全、自由串行化和单一实例问题。调用方式以下:

public static void main(String[] args) {
    //调用输出
    Singleton_06.INSTANCE.testInstace();
}
复制代码

相比之下,你就会发现,枚举实现单例的代码会精简不少。

这种写法虽然在功能上与共有域的方法接近,可是它更简洁。即便在面对复杂的串行化或反射攻击时,也无偿地提供了串行化机制,绝对防止对此实例化。单元素的枚举类型已经成为实现Singleton的最佳方法(我的认为),值得推荐运用!

其实,并非使用枚举就不须要保证线程安全,只不过线程安全的保证不须要咱们关心而已。也就是说,其实在“底层”仍是作了线程安全方面的保证的。这就涉及到jvm的问题,能够经过反编译enum类可了解到底层的东西,今天在这里不赘述。

OK,到这里,经典的七种建立单例模式的方式已经讲完,接下来再说两种方法来达到单例的目的。

利用 CAS“AtomicReference” 实现单例(线程安全)

/** * 利用cas思想实现只有一个实例 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_07 {

    //利用cas思想管理线程安全
    private static final AtomicReference<Singleton_07> INSTANCE = new AtomicReference<Singleton_07>();

    private Singleton_07(){

    }

    //获取对象实例
    public static final Singleton_07 getInstance(){

        for (;;){
            //经过 AtomicReference get方法实现线程安全
            //cas思想 每次都去查询是否存在当前对象
            Singleton_07 instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null,new Singleton_01());
            return INSTANCE.get();
        }
    }

    public static void main(String[] args) {
        //一次请求
        System.out.println(Singleton_07.getInstance());
        //二次请求
        System.out.println(Singleton_07.getInstance());
        
        //类实例同一个
    }
}
复制代码

Java 并发库提供了不少原子类支持并发访问的数据安全性,如:AtomicInteger、AtomicBoolean、AtomicLong 和 AtomicReference。AtomicReference 能够封装引用一个V实例,

上面支持并发访问的单例模式就是利用了这种特性。使用CAS的好处是不须要使用传统的加锁方式,而是依赖CAS的忙等算法、底层硬件的实现保证线程安全。相对于其余锁的实现,没有线程的切换和阻塞也就没有了额外的开销,而且能够支持较大的并发。固然,CAS也有一个缺点就是忙等,若是一直没有获取到,会陷于死循环。

利用 ThreadLocal实现单例

若是你们对ThreadLocal有不明白的地方,能够看个人历史文章: 干货!ThreadLocal 使用场景

/** * 利用ThreadLocal 实现单例,只保存一个对象实例 * @Date 2021/8/1 8:30 下午 * @Author yn */
public class AppContext {
    private static final ThreadLocal<AppContext> local = new ThreadLocal<>();
    private Map<String,Object> data = new HashMap<>();
    public Map<String, Object> getData() {
        return getAppContext().data;
    }
    //批量存数据
    public void setData(Map<String, Object> data) {
        getAppContext().data.putAll(data);
    }
    //存数据
    public void set(String key, String value) {
        getAppContext().data.put(key,value);
    }
    //取数据
    public void get(String key) {
        getAppContext().data.get(key);
    }
    //初始化的实现方法
    private static AppContext init(){
        AppContext context = new AppContext();
        local.set(context);
        return context;
    }
    //作延迟初始化
    public static AppContext getAppContext(){
        AppContext context = local.get();
        if (null == context) {
            context = init();
        }
        return context;
    }
    //删除实例
    public static void remove() {
        local.remove();
    }
}
复制代码

上面的代码实现实际上就是懒汉式初始化的扩展,只不过用 ThreadLocal 替换静态对象来存储惟一对象实例。之所会选择 ThreadLocal,就是由于 ThreadLocal 相比传统的线程同步机制更有优点。

在传统的同步机制中,咱们一般会经过对象的锁机制来保证同一时间只有一个线程访问单例类。这时该类是多个线程共享的,咱们都知道使用同步机制时,何时对类进行读写、何时锁定和释放对象是有很烦琐要求的,这对于通常的程序员来讲,设计和编写难度相对较大

而 ThreadLocal 则会为每个线程提供一个独立的对象副本,从而解决了多个线程对数据的访问冲突的问题。正由于每个线程都拥有本身的对象副本,也就省去了线程之间的同步操做

因此说,如今绝大多数单例模式的实现基本上都是采用的 ThreadLocal 这一种实现方式。

总结

虽然单例模式只是一个很日常的模式,但在各类的实现上却须要用到Java的基本功,包括懒汉模式、饿汉模式、线程是否安全、静态类、内部类、加锁和串行化等。在平常开发中,咱们要根据实际状况去选择其中的一种方式。

感谢您的阅读,创做不易,欢迎点赞,关注, 转发,感谢,你们能够点击我头像查看历史干货文章。

设计模式本月我持续更新,欢迎关注下方的设计模式专栏,我会持续更新 咱们下期再见!

相关文章
相关标签/搜索