Java中单例模式的七种写法示例

目录
  • 前言
  • 1、饿汉式(线程安全)⭐
  • 2、懒汉式(线程不安全)⭐
  • 3、懒汉式(加锁)
  • 4、懒汉式(双重校验锁)⭐
  • 5、单例模式(静态内部类)
  • 6、单例模式(CAS)
  • 7、单例模式(枚举)
  • 总结

前言

大家好,我是三乙己。考上大家一考:“单例模式的单例,怎样写的?”

“不就是构造方法私有化么?”

”对呀对呀!……单例模式有七种写法,你知道么?“

言归正传……

单例模式(Singleton Pattern)可以说是最简单的设计模式了。

用一个成语来形容单例模式——“天无二日,国无二主”。

什么意思呢?就是当前进程确保一个类全局只有一个实例。

那单例模式有什么好处呢?[1]

  • 单例模式在内存中只有一个实例,减少了内存开支
  • 单例模式只生成一个实例,所以减少了系统的性能开销
  • 单例模式可以避免对资源的多重占用
  • 单例模式可以在系统设置全局的访问点

那单例模式是银弹吗?它有没有什么缺点?

  • 单例模式一般没有接口,扩展很困难
  • 单例模式不利于测试
  • 单例模式与单一职责原则有冲突

那什么情况下要用单例模式呢?

  • 要求生成唯一序列号的环境
  • 在整个项目中需要一个共享访问点或共享数据
  • 创建一个对象需要消耗的资源过多
  • 需要定义大量的静态常量和静态方法(如工具类)的环境

接下来,进入今天的主题,我们来看看单例模式的七种写法!

1、饿汉式(线程安全)⭐

public class Singleton_1 {

    private static Singleton_1 instance=new Singleton_1();

    private Singleton_1() {
    }

    public static Singleton_1 getInstance() {
        return instance;
    }

}

饿汉式,就像它的名字,饥不择食,定义的时候直接初始化。

因为instance是个静态变量,所以它会在类加载的时候完成实例化,不存在线程安全的问题。

这种方式不是懒加载,不管我们的程序会不会用到,它都会在程序启动之初进行初始化。

所以我们就有了下一种方式👇

2、懒汉式(线程不安全)⭐

public class Singleton_2 {

    private static Singleton_2 instance;

    private Singleton_2() {
    }

    public static Singleton_2 getInstance() {
        if (instance == null) {
            instance = new Singleton_2();
        }
        return instance;
    }

}

懒汉式 是什么呢?只有用到的时候才会加载,这就实现了我们心心念的懒加载。

但是!

它又引入了新的问题?什么问题呢?线程安全问题。

图片也很清楚,多线程的情况下,可能存在这样的问题:

一个线程判断instance==null,开始初始化对象;

还没来得及初始化对象时候,另一个线程访问,判断instance==null,也创建对象。

最后的结果,就是实例化了两个Singleton对象。

这不符合我们单例的要求啊?怎么办呢?

3、懒汉式(加锁)

public class Singleton_3 {

    private static Singleton_3 instance;

    private Singleton_3() {
    }

    public synchronized static Singleton_3 getInstance() {
        if (instance == null) {
            instance = new Singleton_3();
        }
        return instance;
    }
}

最直接的办法,直接上锁呗!

但是这种把锁直接方法上的办法,所有的访问都需要获取锁,导致了资源的浪费。

那怎么办呢?

4、懒汉式(双重校验锁)⭐

public class Singleton_4 {
    //volatile修饰,防止指令重排
    private static volatile Singleton_4 instance;

    private Singleton_4() {
    }

    public static Singleton_4 getInstance() {
        //第一重校验,检查实例是否存在
        if (instance == null) {
            //同步块
            synchronized (Singleton_4.class) {
                //第二重校验,检查实例是否存在,如果不存在才真正创建实例
                if (instance == null) {
                    instance = new Singleton_4();
                }
            }
        }
        return instance;
    }

}

这是比较推荐的一种,双重校验锁。

它的进步在哪里呢?

我们把synchronized加在了方法的内部,一般的访问是不加锁的,只有在instance==null的时候才加锁。

同时我们来看一下一些关键问题。

首先我们看第一个问题,为什么要双重校验?

大家想一下,如果不双重校验。

如果两个线程一起调用getInstance方法,并且都通过了第一次的判断instance==null,那么第一个线程获取了锁,然后实例化了instance,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了instance。这就不符合我们的单例要求了。

接着我们来看第二个问题,为什么要用volatile 修饰 instance?

我们可能知道答案是防止指令重排。

那这个重排指的是哪?指的是instance = new Singleton(),我们感觉是一步操作的实例化对象,实际上对于JVM指令,是分为三步的:

  • 分配内存空间
  • 初始化对象
  • 将对象指向刚分配的内存空间

有些编译器为为了性能优化,可能会把第二步和第三步进行重排序,顺序就成了:

  • 分配内存空间
  • 将对象指向刚分配的内存空间
  • 初始化对象

所以呢,如果不使用volatile防止指令重排可能会发生什么情况呢?

在这种情况下,T7时刻线程B对instance的访问,访问的是一个初始化未完成的对象。

所以需要在instance前加入关键字volatile。

  • 使用了volatile关键字后,可以保证有序性,指令重排序被禁止;
  • volatile还可以保证可见性,Java内存模型会确保所有线程看到的变量值是一致的。

5、单例模式(静态内部类)

public class Singleton_5 {

    private Singleton_5() {
    }

    private static class InnerSingleton {
        private static final Singleton_5 instance = new Singleton_5();
    }

    public static Singleton_5 getInstance() {
        return InnerSingleton.instance;
    }
}

静态内部类是更进一步的写法,不仅能实现懒加载、线程安全,而且JVM还保持了指令优化的能力。

Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载静态内部类InnerSingleton类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,同时类加载的过程又是线程互斥的,JVM帮助我们保证了线程安全。

6、单例模式(CAS)

public class Singleton_6 {

    private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<Singleton_6>();

    private Singleton_6() {
    }

    public static final Singleton_6 getInstance() {
        //等待
        while (true) {
            Singleton_6 instance = INSTANCE.get();
            if (null == instance) {
                INSTANCE.compareAndSet(null, new Singleton_6());
            }
            return INSTANCE.get();
        }
    }
}

这种CAS式的单例模式算是懒汉式直接加锁的一个变种,sychronized是一种悲观锁,而CAS是乐观锁,相比较,更轻量级。

当然,这种写法也比较罕见,CAS存在忙等的问题,可能会造成CPU资源的浪费。

7、单例模式(枚举)

public enum Singleton_7 {

    //定义一个枚举,代表了Singleton的一个实例
    INSTANCE;
    public void anyMethod(){
        System.out.println("do any thing");
    }
}

调用方式:

    @Test
    void anyMethod() {
        Singleton_7.INSTANCE.anyMethod();
    }

《Effective Java》作者推荐的一种方式,非常简练。

但是这种写法解决了最主要的问题:线程安全、⾃由串⾏化、单⼀实例。

总结

从使用的角度来讲,如果不需要懒加载的话,直接饿汉式就行了;如果需要懒加载,可以考虑静态内部类,或者尝试一下枚举的方式。

从面试的角度,懒汉式、饿汉式、双重校验锁饿汉式,这三种是重点。双重校验锁方式一定要知道指令重排是在哪,会导致什么问题。

到此这篇关于Java中单例模式的七种写法的文章就介绍到这了,更多相关Java单例模式写法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

参考:

[1]. 《设计模式之禅》

[2]. 《重学设计模式》

[3]. 设计模式系列 - 单例模式

[4]. Java中的双重检查锁(double checked locking)

(0)

相关推荐

  • java 单例模式(饿汉模式与懒汉模式)

    java 单例模式 饿汉式单例 对于饿汉模式,我们可这样理解:该单例类非常饿,迫切需要吃东西,所以它在类加载的时候就立即创建对象. 懒汉式单例类 对于懒汉模式,我们可以这样理解:该单例类非常懒,只有在自身需要的时候才会行动,从来不知道及早做好准备.它在需要对象的时候,才判断是否已有对象,如果没有就立即创建一个对象,然后返回,如果已有对象就不再创建,立即返回. 单例设计模式常用于JDBC链接数据库 注意: 1 我们常用的是第一种饿汉式,因为: (1)既然采用了单例设计模式,就是为了使用单例类的对象

  • Java单例模式的应用示例

    单例模式用于保证在程序的运行期间某个类有且仅有一个实例.其优势在于尽可能解决系统资源.通过修改构造方法的访问权限就可以实现单例模式. 代码如下: 复制代码 代码如下: public class Emperor {    private static Emperor emperor = null;// 声明一个Emperor类的引用 private Emperor() {// 将构造方法私有    } public static Emperor getInstance() {// 实例化引用   

  • JAVA多线程并发下的单例模式应用

    单例模式应该是设计模式中比较简单的一个,也是非常常见的,但是在多线程并发的环境下使用却是不那么简单了,今天给大家分享一个我在开发过程中遇到的单例模式的应用. 首先我们先来看一下单例模式的定义: 一个类有且仅有一个实例,并且自行实例化向整个系统提供. 单例模式的要素: 1.私有的静态的实例对象 2.私有的构造函数(保证在该类外部,无法通过new的方式来创建对象实例) 3.公有的.静态的.访问该实例对象的方法 单例模式分为懒汉形和饿汉式 懒汉式: 应用刚启动的时候,并不创建实例,当外部调用该类的实例

  • Java实现单例模式之饿汉式、懒汉式、枚举式

    单例模式的实现(5种) 常用: 饿汉式(线程安全,调用效率高,但是不能延时加载) 懒汉式(线程安全,调用效率不高,可以延时加载) 其他: 双重检测锁式(由于jvm底层内部模型原因,偶尔会出问题,不建立使用) 静态内部类式(线程安全,调用效率高,但是可以延时加载) 枚举单例(线程安全,调用效率高,不能延时加载) 饿汉式单例具体代码如下: package com.lcx.mode; /** * * 饿汉式单例,不管以后用不用这个对象,我们一开始就创建这个对象的实例, * 需要的时候就返回已创建好的实

  • Java单例模式、饥饿模式代码实例

    class MyThreadScopeData {       // 单例     private MyThreadScopeData() {     }       // 提供获取实例方法     public static synchronized MyThreadScopeData getThreadInstance() {         // 从当前线程范围内数据集中获取实例对象         MyThreadScopeData instance = map.get();      

  • java单例模式使用及注意事项

    1. 说明 1)单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例 2)单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式) 3)单例模式要素: a)私有构造方法b)私有静态引用指向自己实例c)以自己实例为返回值的公有静态方法 2.实例 饿单例模式: 复制代码 代码如下: package com.wish.modedesign;

  • JAVA实现单例模式的四种方法和一些特点

    一.饿汉式单例类 复制代码 代码如下: public class Singleton  {      private Singleton(){ } private static Singleton instance = new Singleton(); private static Singleton getInstance(){          return instance;      }  } 特点:饿汉式提前实例化,没有懒汉式中多线程问题,但不管我们是不是调用getInstance()

  • java多线程之线程安全的单例模式

    概念: java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例.饿汉式单例.登记式单例三种. 单例模式有一下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机可以有若干个打印机,但只能有一个Printer

  • java 单例模式和工厂模式实例详解

    单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例. 私有的构造方法 指向自己实例的私有静态引用 以自己实例为返回值的静态的公有的方法 饿汉式单例 public class Singleton { private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return singleton; } } 懒

  • Java单例模式实现静态内部类方法示例

    Singleton是众多设计模式中最容易理解的一种,也是众多设计模式中较为重要的一种设计模式.接下来我们看看具体介绍. Singleton模式实现的重点在于将构造函数私有化(private),并通过提供静态公有函数(public synchronized static xxx getInstance)来获取定义在类中的静态私有成员(private static xxx instance),通过一个简单的判断静态实例是否为空来控制这个类只能够new一次,即控制了一个类只能有单个实例,一般的实现如下

随机推荐