轻松搞定单例模式以及线程安全等问题

2023-05-16

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 定义:保证一个类仅有一个实例,并提供一个全局访问点
  • 类型:创建型
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

适用场景

  • 想确保任何情况下都绝对只有一个实例

优点与缺点

优点:

  • 在内存里只有一个实例,减少内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

缺点:

  • 没有接口,拓展困难

单例模式的重点

  • 私有构造函数,因为单例模式只提供一个全局访问点,不允许将构造函数暴露给用户
  • 在多线程下,可能发生的线程安全问题
  • 是否延迟加载,即大家熟知的饿汉式懒汉式
  • 序列化以及反序列化的安全问题
  • 防止使用反射破坏单例

编码实现单例模式—懒汉式

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

//    与上面的写法是一致的
//    public  static LazySingleton getInstance() {
//        synchronized (LazySingleton.class) {
//            if (lazySingleton == null) {
//                lazySingleton = new LazySingleton();
//            }
//        }
//        //延迟加载
//        return lazySingleton;
//    }
}

在多线程下,为了避免多次创建实例,破坏单例。所以可以使用synchronized来修饰方法,达到同步的目的。对于静态方法来说,锁住的是类的Class对象,即其他线程无法再访问该类,Class对象的加锁与解锁需要消耗资源,且这段代码需要同步的只是下面代码。可以对这种方式进行改进。

if (lazySingleton == null) {
    lazySingleton = new LazySingleton();
}

双重检测懒汉式

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

使用同步代码块,避免了只要访问getInstance方法就锁住Class对象。双重判断也可以避免因为线程调度而产生的实例多次创建的问题。但这样也没完全避免线程安全问题,就是指令重排序带来的问题。这里不讨论指令重排序以及JMM和多线程相关的原理。
new LazyDoubleCheckSingleton()其实包括了3个步骤:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置instance指向内存可见(引用)

2和3是可以调换顺序的,由于2、3之间没有依赖关系,cpu可能会对指令重排序达到提高效率的目的。

image.png
所以可能出上图发生的情况,由于重排序,instance对象并没有完成初始化,但实际上此时instance对象已经不是null。把未初始化的对象发布了出去,这就产生了安全问题!
这里主要讲2种解决方法:禁用指令重排序以及对其他线程指令重排序不可见。

volatile禁用重排序

public class LazyDoubleCheckSingleton {
    //禁用重排序
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    /**
     *由于直接在静态方法使用synchronized,锁的是class对象,即其他线程无法获取此锁。降低性能可以优化。
     *正真需要同步的是延迟加载这个代码块,而不是整个方法。
     * 由于可能发生重排序导致对象未初始化就先完成引用的操作—> lazySingleton !=null,会出现线程安全问题。
     * 所以使用volatile使lazySingleton对所有线程可见。
     */
    public static LazyDoubleCheckSingleton getInstance() {
        //延迟加载
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                    // TODO: 2020/7/4 理解volatile JMM 重排序 happens-before
                    /*
                       1. 分配内存
                       2. 初始化对象
                       3. 将lazySingleton 指向该对象
                       可能会发生指令重排序(仅针对单个处理器中执行的指令序列和单个线程中执行的操作)
                     */
                }
            }
        }
        return lazySingleton;
    }
}

volatile禁用重排序的原理是内存屏障,这里不作讨论。

基于静态内部类的懒加载

public class StaticInnerClassSingleton {
    private  static class InnerClass{
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {

    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }
}

image.png

首先对于内部类,static final修饰的常量,在类的初始化阶段就完成赋值了。并且类的初始化具有Class对象锁,只有一个线程可获取到Class对象锁。所以就算发生了重排序对于其他线程也是不可见的。从而屏蔽了重排序问题。

编码实现单例模式—饿汉式

public class HungrySingleton implements Serializable {
    public static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {
        if (HUNGRY_SINGLETON!=null) {
            throw new RuntimeException("禁止在单例模式使用反射");
        }
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

    private Object readResolve() {
        return HUNGRY_SINGLETON;
    }
}

饿汉式是比较容易实现的,且没有线程安全问题。因为instance为常量,在类加载是就已经赋值了。
上面例子种增加了一些无关代码,下面再来解释一下原因,主要是针对反射破坏以及序列化破坏单例方面。

破坏单例模式的一些问题

主要包括:

  • 在多线程下,可能发生的线程安全问题
  • 序列化以及反序列化的安全问题
  • 防止使用反射破坏单例

线程安全问题已经在上面说明了,就不赘述。

防止使用反射破坏单例

Class<HungrySingleton> objectClass = HungrySingleton.class;
Constructor<HungrySingleton> constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();

我们可以通过反射来调用单例类的私有构造器,从而破坏了单例模式。

 private HungrySingleton() {
        if (HUNGRY_SINGLETON!=null) {
            throw new RuntimeException("禁止在单例模式使用反射");
        }
    }

这个时候,这段的代码的作用就看出来,由于在类加载阶段就已经生成了instance对象,我们在构造器对instance进行判断,抛出异常。
可以有效防止外部通过反射调用私有构造函数。但对于懒汉式,就很难防止反射攻击,因为懒汉式的instance对象是在getInstance方法中生成的,所以无论采取什么方式,比如:标志位(flag),反射都可以获取到变量,修改变量来达到new一个新的instance的目的。

序列化以及反序列化的安全问题

  HungrySingleton instance = HungrySingleton.getInstance();
  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry_singleton"));
  oos.writeObject(instance);
  ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry_singleton"));
  HungrySingleton newInstance = (HungrySingleton) ois.readObject();
  System.out.println(instance.getData() == newInstance.getData());

image.png
通过结果,输入输出的对象不是同一个对象。那么为什么两次的结果不一致,这里只展出jdk的关键源码。

/* 定义在ObjectInputSteam的readOrdinaryObject方法
该类是否包含有构造器,有则返回true,当返回true时,通过反射new一个instance
*/
obj = desc.isInstantiable() ? desc.newInstance() : null;
boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
}

/*
当类里包含有readResolve方法,就会调用readResolve的返回值作为序列化的返回值,不然将返回返回上面新new的对象
*/
if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
        }
boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }
 readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);

主要的逻辑就是先判断该类是不是可以支持序列化,然后先通过反射new一个instance,之后再判断类里是否有readResolve方法,有则返回readResolve方法返回的object,没有则返回新new出的object。所以在单例类里补充readResolve方法就可以解决序列化的问题。

 private Object readResolve() {
        return HUNGRY_SINGLETON;
    }

image.png

单例模式最佳实践—基于枚举类

public enum  EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

我们使用jad来反编译这个类:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumInstance.java

package com.qzh.design.pattern.creational.singleton;


public final class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/qzh/design/pattern/creational/singleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

可以看到INSTANCE在类加载阶段就初始化了,所以是线程安全的。且具有一个私有的构造函数,符合单例模式。
并且枚举类是不支持反射的。在Constructor的newInstance方法中

if ((this.clazz.getModifiers() & 16384) != 0) {
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        }

可以看到无法使用构造器来反射出枚举类对象。
枚举类也天生支持序列化,我们继续来看ObjectInputSteam的代码:

String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
  try {
         @SuppressWarnings("unchecked")
         Enum<?> en = Enum.valueOf((Class)cl, name);
         result = en;
 } catch (IllegalArgumentException ex) {
        throw (IOException) new InvalidObjectException(
         "enum constant " + name + " does not exist in " +
       cl).initCause(ex);
   }
 if (!unshared) {
        handles.setObject(enumHandle, result);
     }
 }

可以看到,是通过名称去获取枚举对线,而枚举对象又是static final修饰的,所以拿到的都是同一个对象,有兴趣的朋友可以深入到Eunm的valueOf方法去分析。

后记

总结了常见的单例模式的一些具体实现,以及一步步去优化代码,解决问题。以及解释大家平时不常用的用枚举类来实现单例。希望大家有所收获!
github地址
源码:https://github.com/DiangD/design_pattern_practice

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

轻松搞定单例模式以及线程安全等问题 的相关文章

随机推荐

  • Python练习:求100以内的素数和

    练习 xff1a 求100以内的素数和 平台 xff1a Python123 io 教师 xff1a 嵩天 求100以内的素数和 描述 求100以内的素数之和并输出 def sushu n for i in range 2 n if n i
  • Python练习:四叶玫瑰数

    练习 xff1a 四叶玫瑰数 平台 xff1a Python123 io 教师 xff1a 嵩天 描述 四叶玫瑰数是4位数的自幂数 自幂数是指一个 n 位数 xff0c 它的每个位上的数字的 n 次幂之和等于它本身 xff08 例如 xff
  • 使用云服务器搭建Hexo个人博客

    彻底搞懂如何使用Hexo 43 GitHubPages搭建个人博客 讲解了如何使用Hexo 43 GitHubPages来搭建一款属于自己的个人博客 使用了一段时间后 xff0c 发现访问速度偏慢 xff0c 而且自己也在前段时间腾讯云做活
  • Python追赶法解方程组

    A 61 定义一个列表存储系数矩阵 L 61 U 61 Y 61 a 61 1 b 61 4 c 61 1 S 61 a S T 61 T R 61 R Answer X 61 N 61 5 X 61 39 x1 39 39 x2 39 3
  • 手把手教你安装Spring+搭建Spring开发环境

    为了让新手安装Spring时少走弯路 xff0c 小编写下详细步骤 xff08 以win8 xff0c 64位为例 xff09 请系好安全带 xff0c 坐好扶稳 一 xff1a 安装eclipse 如果已经安装请忽略此步 官网下载地址 x
  • 在Ubuntu上如果权限不够时,并且使用sudo还是权限不够,获得root权限

    如果用户具有sudo权限 xff0c 那么直接可以运行如下命令 xff1a sudo su root 输入当前用户的密码 passwd 输入密码 再次输入密码 退出root权限 按下ctrl 43 D
  • yum 安装libpcap

    针对于centos系统安装libpcap库命令 xff1a yum install libpcap yum install libpcap devel
  • 计算机网络自考2020,2020年8月自考02141计算机网络技术真题及答案

    以下是湖南自考生网为考生们整理的 2020年8月自考02141计算机网络技术真题及答案 xff0c 考生可通过自考历年真题练习更有把握的面对考试 xff0c 对题型更加熟悉 xff0c 从而取得更佳的成绩 供考生参考 2020年8月高等教育
  • DLL丢失一键修复

    DLL丢失一键修复 今天不小心删了DLL相关的库 xff0c 费了好久才搞好 xff0c 记录一下 原因 xff1a 删除了win10中的这些库 解决办法 xff1a 使用一键修复工具 DLL丢失一键修复工具 xff1a 获取工具
  • springmvc的配置文件详解

    springmvc xml需要配置的东西 配置controller扫描包 使用组件扫描器省去在spring容器配置每个Controller类的繁琐 使用context component scan自动扫描标记 64 Controller的控
  • Bug随手记----关于java.lang.IllegalStateException: The following classes could not be excluded because the

    Consider the following If you want an embedded database H2 HSQL or Derby please put it on the classpath If you have data
  • linux:Systemd使用(systemctl)

    系统服务管理工具systemd是为了便于linux用户操作系统服务的管理 xff0c systemd提供了systemctl命令工具进行systemd的各项操作 Systemd的版本 systemd version systemd和syst
  • C++面试宝典:进程间通讯方式概述

    1 管道 我们来看一条 Linux 的语句 netstat tulnp grep 8080 学过 Linux 命名的估计都懂这条语句的含义 xff0c 其中 是管道的意思 xff0c 它的作用就是把前一条命令的输出作为后一条命令的输入 在这
  • ubuntu16.04LTS更换阿里源

    sudo gedit etc apt sources list 替换 xff1a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 deb cdrom Ubuntu 16 04 LTS Xenial
  • a++多线程下出现消失的请求现象

    a 43 43 多线程下出现消失的请求现象 a 43 43 多线程下出现消失的请求现象是一个常见的多线程运行结果错误的例子 我们先来看一下a 43 43 代码执行图 xff1a 按照我们想让它执行的顺序执行 xff0c 结果应该是3 但在多
  • 2013年十佳优秀主席将获得传智播客.NET学院培训名额

    2013年CSDN高校俱乐部十佳优秀主席将会免费获得价值9580元的传智播客 NET学院免费培训名额 培训时间为2015年之前有效 优秀主席评选截止日期为10月底 截止日期之前请提交你的评选内容 评选内容是俱乐部介绍 自我介绍和俱乐部活动介
  • MySQL各数据类型的特点

    MySQL各数据类型的特点 常用的整数类型 常见误区 int n 是设置数据最大的显示宽度 xff0c 所占的存储空间不会改变 常用的浮点类型 123456789 987654321 61 decimal 18 9 占用9个字节 涉及财务等
  • Redis持久化——RDB、AOF

    Redis持久化 RDB AOF 什么是持久化 redis所有数据都保存在内存中 xff0c 对数据的更新异步保存到磁盘上 在Redis中持久化的方式有两种 xff0c 一种是快照持久化 xff0c 一种是AOF持久化 xff0c 各有各的
  • Redis主从复制—看完这篇你就懂了

    Redis主从复制 主从复制 xff0c 是指将一台Redis服务器的数据 xff0c 复制到其他的Redis服务器 前者称为主节点 master leader xff0c 后者称为从节点 slave follower xff1b 数据的复
  • 轻松搞定单例模式以及线程安全等问题

    单例模式 单例模式 xff08 Singleton Pattern xff09 是 Java 中最简单的设计模式之一 这种类型的设计模式属于创建型模式 xff0c 它提供了一种创建对象的最佳方式 这种模式涉及到一个单一的类 xff0c 该类