【设计模式】秒懂单例模式

共 6188字,需浏览 13分钟

 ·

2022-07-04 17:34

单例模式介绍

概述

单例模式:某一个类在系统中只需要有一个实例对象,而且对象是由这个类自行实例化并提供给系统其它地方使用,这个类称为单例类。单例模式是GOF 23种设计模式中最简单的一种,但同时也是在项目中接触最多的一种。单例模式属于一种创建型设计模式。

使用场景

大家都使用过Windows任务管理器,正常情况下,无论我们在Windows任务栏的右键菜单上点击启动多少次“任务管理器”,系统始终只能弹出一个任务管理器窗口。也就是说,在一个Windows系统中,系统只维护一个任务管理器。这就是一个典型的单例模式运用。

再举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制。

可以看出,我们在程序中使用单例模式,目的一般是处理资源访问的冲突,或者从业务概念上,有些数据在系统中只应保存一份,那也比较适合设计为单例类,比如配置类、全局流水号生成器等。

UML类图 

单例模式实现

单例模式实现要点

单例模式虽然简单,但是要写出一个能保证在多线程环境下也能保证实例唯一性的单例确不是那么简单,实现一个正确的单例模式有以下几个要点:

  • • 1.某个类只能有一个实例,即使是多线程运行环境下;

  • • 2.单例类的实例一定是单例类自身创建,而不是在单例类外部用其它方式如new方式创建;

  • • 3.单例类需要提供一个方法向整个系统提供这个实例对象。

单例两种模式

单例模式分为饿汉模式懒汉模式,这两种模式很好理解,懒汉模式的意思就是这个类很懒,只要别人不找它要实例,它都懒得创建。饿汉模式在初始化时,我们就创建了唯一的实例,即便这个实例后面并不会被使用。

下面分别介绍两种单例模式的写法。

懒汉式

下面这种写法的单例是大家最简单最容易写出的一种单例写法,只适用于单线程的系统,也就是说它不是线程安全的。

//懒汉式,线程不安全
 class Singleton1{
     private static  Singleton1 instance;

     //构造函数定义为私有,防止外部创建实例
     private Singleton1(){

     }

     //系统使用单例的入口
     public static Singleton1 getInstance(){
         if (null == instance){
             instance = new Singleton1();
         }

         return instance;
     }
 }

针对线程不安全的问题,可以通过获取实例的方法添加了synchronized来解决,如下:

//懒汉式,线程安全,效率低
class Singleton2{
    private static  Singleton2 instance;

    //构造函数定义为私有,防止外部创建实例
    private Singleton2(){

    }

    //系统使用单例的入口
    public static 
synchronized Singleton2 getInstance(){
        if (null == instance){
            instance = new Singleton2();
        }

        return instance;
    }
}

这样一来,确实线程安全了,但是又带来了另一个问题:程序的性能极大的降低了,高并发下多个线程去获取这个实例,现在却要排队。

针对性能问题,有同学想到了减小synchronized的粒度,不加在方法上,而是放在代码块中:

//懒汉式,线程不安全
class Singleton3{
    private static  Singleton3 instance;

    //构造函数定义为私有,防止外部创建实例
    private Singleton3(){

    }

    //系统使用单例的入口
    public static Singleton3 getInstance(){
        if (null == instance){
            
synchronized(Singleton3.class) {
                instance = new Singleton3();
            }
        }

        return instance;
    }
}

但是,很不幸,如果改成这样,又变得线程不安全了,我们试着分析一个代码执行的场景:假设我们有两个线程 T1与T2并发访问getInstance方法。当T1执行完if (instance == null)且instance为null时,其CUP执行时间被T2抢占,所以T1还没有创建实例。T2也执行if (instance == null),此时instance肯定还为null,T2执行创建实例的代码,当T1再次获得CPU执行时间后,其从synchronized 处恢复,又会创建一个实例。

那么有没有一种写法,可以同时兼顾到效率和线程安全两方面了,还真有,就是我们下面将要介绍的double-check的方式。

////懒汉式,线程安全,效率还可以
class Singleton4{
     //注意加上volatile关键字
    private static  volatile Singleton4 instance;

    //构造函数定义为私有,防止外部创建实例
    private Singleton4(){

    }

    //系统使用单例的入口
    public static Singleton4 getInstance(){
        //第一次检查提高访问性能
     
   if (null == instance){
            synchronized(Singleton4.class) {
                //第二次检查为了线程安全
                
if(instance ==null) {
                    instance = new Singleton4();
                }
            }
        }

        return instance;
    }
}

这种单例的写法做了两次 if (null == instance)的判断,因此被称为double-check的方式。

  • • 第一次check为了提高访问性能。因为一旦实例被创建,后面线程的所有的check都为假,不需要执行synchronized竞争锁了。

  • • 第二次check是为了线程安全,确保多线程环境下只生成一个实例。

需要注意的是,这种方式,在定义实例时一定需要加上volatile 关键字,禁止虚拟机指令重排,否则,还是有一定几率会生成多个实例,关于volatile 关键字和指令重排的问题这里不过多介绍,后面在多线程安全系列文章中再详细介绍。

饿汉式

使用静态常量在类加载时候就创建了实例,属于饿汉模式。其是线程安全的,这一点由JVM来保证。

//饿汉式,线程安全
class Singleton5{
     //
    
private static final Singleton5 INSTANCE = new Singleton5();

    //构造函数定义为私有,防止外部创建实例
    private Singleton5(){

    }

    //系统使用单例的入口
    public static Singleton5 getInstance(){
        return INSTANCE;
    }
}

本文源码地址:
https://github.com/qinlizhong1/javaStudy/tree/master/DesignPattern/src/singleton

本文示例代码环境:
操作系统:macOs 12.1
JDK版本:12.0.1
maven版本: 3.8.4

推荐阅读:

完全整理 | 365篇高质技术文章目录整理

算法之美 : 栈和队列

主宰这个世界的10大算法

彻底理解cookie、session、token

浅谈什么是递归算法

专注服务器后台技术栈知识总结分享

欢迎关注交流共同进步

浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报