Java多线程之不可变对象(Immutable Object)模式

news2024/11/16 9:32:08

简介

多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。Immutable Object模式使得我们可以在不加锁的情况下,既保证共享变量访问的线程安全,又能避免引入锁可能带来的问题和开销。

多线程环境中,一个对象常常会被多个线程共享,这种情况下,如果存在多个线程并发的修改该对象的状态或者一个线程访问该对象的状态而另外一个线程试图修改该对象的状态,我们不得不做一些同步访问控制以保证数据一致性。而这些同步访问控制,如显式锁(Explicit Lock)和CAS操作,会带来额外的开销和问题,如上下文切换、等待时间、ABA问题。Immutable Object模式的意图是通过使用对外可见的状态不可变的对象,使得被共享的对象“天生”具有线程安全性,而无需额外的同步访问控制。从而既保证了数据一致性,又避免了同步访问控制所产生的额外开销和问题,也简化了编程。

什么是状态不可变的对象:即对象一经创建,其对外可见的状态就保持不变,通过下面的例子辅助理解。

一个车辆管理系统要对车辆的位置信息进行跟踪,我们可以对车辆的位置信息建立如下所示模型:

public class Location {
    private double x;
    private double y;
    public Location(double x, double y) {
        this.x = x;
        this.y = y;
    }
    public double getX() {
        return x;
    }
    public double getY() {
        return y;
    }
    public void setXY(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

当系统接收到新的车辆坐标数据时,需要调用Location的setXY方法来更新位置信息。显然,代码中的setXY()是非线程安全的,因为对坐标数据x和y的写操作不是一个原子操作。setXY被调用时,如果在x写入完毕,而y开始写之前有其他线程来读取位置信息,则该线程可能读到一个被追踪车辆根本不曾经过的位置。为了使setXY方法具备线程安全性,我们需要借助锁进行访问控制,虽然被追踪车辆的位置信息总是在变化,但是我们也可以将位置信息建模为状态不可变的对象。

状态不可变的位置信息模型

public final class ImmutableLocation {
    public final double x;
    public final double y;

    public ImmutableLocation(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

使用状态不可变的位置信息模型时,如果车辆的位置发生变动,则更新车辆的位置信息是通过替换表示位置信息的对象(即Location实例)来实现的。因此,所谓状态不可变的对象并非指被建模的现实世界实体的状态不可变,而是我们在建模的时候的一种决策:现实世界实体的状态总是变化的,但我们可以用状态不可变的对象来对这些实体进行建模。

Immutable Object模式的架构

Immutable Object模式将现实世界中状态可变的实体建模为状态不可变的对象,并通过创建不同的状态不可变的对象来反映实体的状态变更。

Immutable Object模式的主要参与者有以下几种。其类图如下
在这里插入图片描述

ImmutableObject: 负责存储一组不可变状态,该参与者不对外暴露任何可以修改其状态的方法,主要方法及职责如下:

  • getStateX、getStateN:这些getter方法返回其所属ImmutableObject实例所维护的状态相关变量的值,这些变量在对象实例化时通过其构造器的参数获得值。
  • getStateSnapshot:返回其所属ImmutableObject实例维护的一组状态的快照。

Manipulator:负责维护ImmutableObject所建模的现实世界实体状态的变更。当相应的现实实体状态变更时,该参与者负责生成新的ImmutableObject实例,以反映新的状态。

  • changeStateTo:根据新的状态值生成新的ImmutableObject实例。

不可变对象的使用主要包括以下几种类型:

  • 获取单个状态的值;调用不可变对象的相关getter方法即可实现。
  • 获取一组状态的快照:不可变对象可以提供一个getter方法,该方法需要对其返回值做防御性复制或者返回一个只读的对象,以避免其状态对外泄漏而被改变。
  • 生成新的不可变对象实例:当被建模对象的状态发生变化的时候,创建新的不可变对象实例来反映这种变化。

Immutable Object模式的典型交互场景序列图如下:
在这里插入图片描述
第1-4步:客户端代码获取当前ImmutableObject实例的各个状态值。
第5步:客户端代码调用Manipulator的changeStateTo方法来更新应用的状态。
第6-7步:changeStateTo方法创建新的ImmutableObject实例以反映应用的新状态,并返回。
第8-9步:客户端代码获取新的ImmutableObject实例的状态快照。

一个严格意义上的不可变对象要满足以下所有条件

  1. 类本身使用final修饰,防止其子类改变其定义的行为。
  2. 所有字段都是final修饰:使用final修饰不仅仅是从语义上说明被修饰字段的引用不可变。更重要的是这个语义在多线程环境下由JMM保证了被修饰字段所引用对象的初始化安全。即final修饰的字段在其他线程可见时,他必定是初始化完成的。相反,非final修饰的字段由于缺少这种保证,可能导致一个线程“看到”一个字段的时候,他还未被初始化完成,从而导致一些不可预料的结果(参考Java类加载过程)。
  3. 在对象的创建过程中,this关键字没有泄漏给其他类:防止其他类(如该类的内部匿名类)在对象创建过程中修改其状态。
  4. 任何字段,若其引用了其他状态不可变的对象(如集合、数组等),则这些字段必须是private修饰的。并且这些字段值不能对外暴露。若有相关方法要返回这些字段值,应用进行防御性复制(Defensive Copy)。

Immutable Object模式案例

某彩信网关系统在处理由增值业务提供商下发给手机终端用户的彩信消息时,需要根据彩信接收方号码的前缀选择对应的彩信中心。然后转发消息给选中的彩信中心,由其负责对接电信网络将彩信消息下发给手机终端用户。彩信中心相对于彩信网关系统而言,他是一个独立的部件,二者通过网络进行交互。这个选择彩信中心的过程,我们称之为路由(Routing)。而手机号前缀和彩信中心的这种对应关系,我们称之为路由表。
路由表在软件运维过程中可能发生变化。例如,业务扩容带来的新增彩信中心、为某个号码前缀指定新的彩信中心。虽然路由表在该系统中是由多线程共享的数据,但是这些数据的变化频率并不高。 因此,即使为了保证线程安全,我们也不希望对这些数据的访问进行加锁等并发访问控制,以免产生不必要的开销和问题。这时Immutable Object模式就派上用场了。

使用不可变对象维护路由表,代码清单

package io.github.viscent.mtpattern.ch3.immutableobject;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * 彩信中心路由规则管理器 模式角色:ImmutableObject.ImmutableObject
 */
public final class MMSCRouter {
    // 用volatile修饰,保证多线程环境下该变量的可见性
    private static volatile MMSCRouter instance = new MMSCRouter();
    // 维护手机号码前缀到彩信中心之间的映射关系
    private final Map<String, MMSCInfo> routeMap;

    public MMSCRouter() {
        // 将数据库表中的数据加载到内存,存为Map
        this.routeMap = MMSCRouter.retrieveRouteMapFromDB();
    }

    private static Map<String, MMSCInfo> retrieveRouteMapFromDB() {
        Map<String, MMSCInfo> map = new HashMap<String, MMSCInfo>();
        // 省略其他代码
        return map;
    }

    public static MMSCRouter getInstance() {

        return instance;
    }

    /**
     * 根据手机号码前缀获取对应的彩信中心信息
     * 
     * @param msisdnPrefix
     *            手机号码前缀
     * @return 彩信中心信息
     */
    public MMSCInfo getMMSC(String msisdnPrefix) {
        return routeMap.get(msisdnPrefix);

    }

    /**
     * 将当前MMSCRouter的实例更新为指定的新实例
     * 
     * @param newInstance
     *            新的MMSCRouter实例
     */
    public static void setInstance(MMSCRouter newInstance) {
        instance = newInstance;
    }

    private static Map<String, MMSCInfo> deepCopy(Map<String, MMSCInfo> m) {
        Map<String, MMSCInfo> result = new HashMap<String, MMSCInfo>();
        for (String key : m.keySet()) {
            result.put(key, new MMSCInfo(m.get(key)));
        }
        return result;
    }

    public Map<String, MMSCInfo> getRouteMap() {
        // 做防御性拷贝
        return Collections.unmodifiableMap(deepCopy(routeMap));
    }

}

而彩信中心的相关数据,如彩信中心设备编号、URL、支持的最大附件尺寸也被建模为一个不可变对象,如下所示:

package io.github.viscent.mtpattern.ch3.immutableobject;

/**
 * 彩信中心信息
 * 
 * 模式角色:ImmutableObject.ImmutableObject
 */
public final class MMSCInfo {
    /**
     * 设备编号
     */
    private final String deviceID;
    /**
     * 彩信中心URL
     */
    private final String url;
    /**
     * 该彩信中心允许的最大附件大小
     */
    private final int maxAttachmentSizeInBytes;

    public MMSCInfo(String deviceID, String url, int maxAttachmentSizeInBytes) {
        this.deviceID = deviceID;
        this.url = url;
        this.maxAttachmentSizeInBytes = maxAttachmentSizeInBytes;
    }

    public MMSCInfo(MMSCInfo prototype) {
        this.deviceID = prototype.deviceID;
        this.url = prototype.url;
        this.maxAttachmentSizeInBytes = prototype.maxAttachmentSizeInBytes;
    }

    public String getDeviceID() {
        return deviceID;
    }

    public String getUrl() {
        return url;
    }

    public int getMaxAttachmentSizeInBytes() {
        return maxAttachmentSizeInBytes;
    }
}

彩信中心信息变更的频率也同样不高。因此,当彩信网关系统通过网络(Socket连接)被通知到彩信中心信息本身或者路由表变更时,网关系统会重新生成新的MMSCInfo和MMSCRouter来反映这种变更。

package io.github.viscent.mtpattern.ch3.immutableobject;

/**
 * 与运维中心(Operation and Maintenance Center)对接的类<BR>
 * 模式角色:ImmutableObject.Manipulator
 */
public class OMCAgent extends Thread {

    @Override
    public void run() {
        boolean isTableModificationMsg = false;
        String updatedTableName = null;
        while (true) {
            // 省略其他代码
            /*
             * 从与OMC连接的Socket中读取消息并进行解析, 解析到数据表更新消息后,重置MMSCRouter实例。
             */
            if (isTableModificationMsg) {
                if ("MMSCInfo".equals(updatedTableName)) {
                    MMSCRouter.setInstance(new MMSCRouter());
                }
            }
            // 省略其他代码
        }
    }
}

上述代码会调用MMSCRouter.setInstance方法来替换MMSCRouter的实例为新创建的实例。而新创建的MMSCRouter实例通过其构造器会生成多个新的MMSCInfo的实例。

本案例中,MMSCInfo是一个严格意义上的不可变对象,虽然MMSCRouter对外提供了setInstance方法用于改变其静态字段instance的值,但他仍然可被视作一个等效的不可变对象。这是因为setInstance方法仅仅改变instance变量指向的对象,而instance变量采用volatile修饰保证了其在多线程之间的内存可见性,所以这意味着setInstance对instance变量的改变无须加锁也能保证线程安全。而其他代码在调用MMSCRouter 的相关方法获取路由信息时也无须加锁。

OMCAgent类是一个Manipulator参与者实例,而MMSCInfo和MMSCRouter是一个Immutableobject 参与者实例。通过使用不可变对象,我们既可以应对路由表、彩信中心这些不是非常频繁的变更,又可以使系统中使用路由表的代码免于并发访问控制的开销和问题。

Immutable Object模式评价与实现考量

不可变对象具有天生的线程安全性,多个线程共享一个不可变对象的时候无须使用额外的并发访问控制,使得我们可以避免显式锁等并发访问控制的开销和问题,简化了多线程编程。

Immutable Object模式特别适用于以下场景

  • 被建模的对象的状态变化不频繁:这种场景下可以设置一个专门的线程(Manipulator参与者所在的线程)用于在被建模对象状态变化时创建新的不可变对象。而其他线程只是读取不可变对象状态。
  • 同时对一组相关的数据进行写操作,因此需要保证原子性:此场景为了保证操作的原子性,通常做法是使用显式锁。但若采用Immutable Object模式,将这一组相关的数据“组合”成一个不可变对象,则对这一组数据的操作就无须加显式锁也能保证原子性。
  • 使用某个对象作为安全的HashMap的key:一个对象作为HashMap的key被放入HashMap之后,若该对象状态变化导致了其HashCode 的变化,则会导致后面在用同样的对象作为Key去get的时候无法获取关联的值。由于不可变对象的状态不变,因此其HashCode也不变,使得不可变对象非常适于用作HashMap的key。

Immutable Object模式实现时需要注意以下问题

  • 被建模的对象状态变更频繁:此时也不见得不能使用Immutable Object模式,只是这意味着频繁创建新的不可变对象,因此会增加JVM垃圾回收的负担和CPU消耗,需要综合考虑:被建模对象的规模、代码目标运行环境的JVM内存分配情况、系统对吞吐量和响应性要求。
  • 使用等效或者近似的不可变对象:有时创建严格意义上的不可变对象比较难,但是尽量向严格意义上的不可变对象靠拢也有利于发挥不可变对象的好处。
  • 防御性复制:如果不可变对象本身包含一些状态需要对外暴露,而相应的字段本身又是可变的(如HashMap)。那么返回这些字段的方法还是需要做防御性复制,以避免外部代码修改了其内部状态。

JDK应用Immutable Object模式实例

在多线程环境中,遍历一个集合对象时,即便被遍历的对象本身是线程安全的,开发人员仍然不得不引入锁,以防止遍历过程中该集合的内部结构被其他线程改变(如删除或者插入一个新的元素)而导致出错。

 Vector vector = null;
 synchronized (vector)(
 for (int i = 0; i < vector.size ; i++) {
       .....
 }

为了保证线程安全而在遍历时对集合对象进行加锁,但这在某些场景下可能并不合适,比如系统中对该集合的插入和删除操作频率远大于便利操作频率。JDK1.5中引入的CopyOnWriteArrayList应用了Immutable Object模式,使得对CopyOnWriteArrayList实例进行遍历时不用加锁也能保证线程安全。他是专门针对遍历操作的频率比添加和删除操作更加频繁的场景设计的(CopyOnWriteArrayList源码解析)。

CopyOnWriteArrayList内部维护了一个array的实例变量用于存储集合的各个元素。在集合中添加一个元素的时候,

CopyOnWriteArrayList会生成一个新的数组,并将集合中所有的元素都复制到新的数组,然后将新的数组的最后一个变量设置为要添加的元素。这个新的数组会直接赋值给array实例变量。这里,array引用的数组就是一个等效的Immutable Object。其内容一旦确定下来就不再被改变,因此,遍历的CopyOnWriteArrayList维护各个元素的时候,直接根据array实例变量生成一个Iterator实例即可,无须加锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1812752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何用二维码进行来访登记?这个模板帮你轻松实现!

在工厂、学校、写字楼、建筑工地等人员出入频繁的场所&#xff0c;使用传统的纸质登记方法容易造成数据丢失&#xff0c;而且信息核对过程繁琐&#xff0c;效率低下。 可以用二维码代替纸质登记本&#xff0c;访客进入时扫码就能登记身份信息&#xff0c;能够提高门岗访客管理…

微生信神助力:在线绘制发表级主成分分析(PCA)图

主成分分析&#xff08;Principal components analysis&#xff0c;PCA&#xff09;是一种线性降维方法。它利用正交变换对一系列可能相关的变量的观测值进行线性变换&#xff0c;从而投影为一系列线性不相关变量的值&#xff0c;这些不相关变量称为主成分&#xff08;Principa…

JMH309【亲测】典藏3D魔幻端游【剑踪3DⅢ】GM工具+开区合区工具+PC客户端+配置修改教程+Win一键服务端+详细外网视频教程

资源介绍&#xff1a; 经典不错的一款端游 GM工具开区合区工具PC客户端配置修改教程Win一键服务端详细外网视频教程 资源截图&#xff1a; 下载地址

数字化医疗:揭秘物联网如何提升医院设备管理效率!

在当今数字化时代&#xff0c;医疗领域正迎来一场技术变革的浪潮&#xff0c;而基于物联网的智慧医院医疗设备管理体系正是这场变革的闪耀之星。想象一下&#xff0c;医院里的每一台医疗设备都能像一位精密的工匠一样&#xff0c;自动监测、精准诊断&#xff0c;甚至在发生故障…

GitLab教程(三):多人合作场景下如何pull代码和处理冲突

文章目录 1.拉取别人同步的代码到本地的流程2.push冲突发生场景情景模拟简单的解决方法 在这一章中&#xff0c;为了模拟多人合作的场景&#xff0c;我需要一个人分饰两角。 执行git clone xx远端仓库地址 xx文件夹命令&#xff0c;在clone代码时指定本地仓库的文件夹名&#…

33.星号三角阵(二)

上海市计算机学会竞赛平台 | YACSYACS 是由上海市计算机学会于2019年发起的活动,旨在激发青少年对学习人工智能与算法设计的热情与兴趣,提升青少年科学素养,引导青少年投身创新发现和科研实践活动。https://www.iai.sh.cn/problem/742 题目描述 给定一个整数 𝑛,输出一个…

解决:RuntimeError: “slow_conv2d_cpu“ not implemented for ‘Half‘的方法之一

1. 问题描述 今天跑实验的时候&#xff0c;代码报错&#xff1a; RuntimeError: "slow_conv2d_cpu" not implemented for Half 感觉有点莫名奇妙&#xff0c;经检索&#xff0c;发现将fp16改为fp32可以解决我的问题&#xff0c;但是运行速度太慢了。后来发现&…

基于WPF技术的换热站智能监控系统02--标题栏实现

1、布局划分 2、准备图片资源 3、界面UI控件 4、窗体拖动和关闭 5、运行效果 走过路过不要错过&#xff0c;点赞关注收藏又圈粉&#xff0c;共同致富&#xff0c;为财务自由作出贡献

理解线程安全:保护你的代码免受并发问题困扰

目录 前言 一、什么是线程安全&#xff1f; 二、为什么需要线程安全&#xff1f; 三、实现线程安全的方法 四、synchronized 使用 synchronized 关键字时&#xff0c;需要注意以下几点&#xff1a; 五、Demo讲解 前言 在现代软件开发中&#xff0c;尤其是在多线程编程中&…

【源码】二开版微盘交易系统/贵金属交易平台/微交易系统

二开版微盘交易系统/贵金属交易平台/微交易系统 一套二开前端UI得贵金属微交易系统&#xff0c;前端产品后台可任意更换 此系统框架不是以往的至尊的框架&#xff0c;系统完美运行&#xff0c;K线采用nodejs方式运行 K线结算都正常&#xff0c;附带教程 资源来源:https://www.…

C++ UML建模

starUML UML图转C代码 数据流图 E-R图 流程图 整体架构图 ORM关系图 参考 app.asar附件资源可免激活 JHBlog/设计模式/设计模式/1、StarUML使用简明教程.md at master SunshineBrother/JHBlog GitHub GitHub - dimon4ezzz/whitestaruml: UML modeling tool derived from …

汇编语言期末复习

目录 前言 基础知识 80x86计算机组织 80x86的寻址方式 前言 根据老师的PPT与IBM-PC汇编语言程序设计&#xff08;第2版&#xff09;而写&#xff0c;供考前突击所用。 基础知识 q 机器语言、汇编语言、高级程序语言 特性 比较 q 进位记数制与不同基数的数之间的转换 二进…

可变参数以及不可变集合

可变参数&#xff1a; 格式&#xff1a; public class ArgsDemo {public static void main(String[] args) {System.out.println(getSum(1,2,3,4,5));}//可变参数public static int getSum(int...args){int sum 0;for (int arg : args) {sum arg;}return sum;} }可变参数的…

笨蛋学算法之LeetCodeHot100_1_两数之和(Java)

package com.lsy.leetcodehot100;public class _Hot1_两数之和 {//自写方法public static int[] twoSum1(int[] nums, int target) {//定义存放返回变量的数组int[] arr new int[2];//遍历整个数组for (int i 0; i < nums.length; i) {//从第二个数开始相加判断for (int j…

RK3588 Debian11进行源码编译安装Pyqt5

RK3588 Debian11进行源码编译安装Pyqt5 参考链接 https://blog.csdn.net/qq_38184409/article/details/137047584?ops_request_misc%257B%2522request%255Fid%2522%253A%2522171808774816800222841743%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&…

SpringBoot内置数据源

回顾: 在我们之前学习在配置文件当中配置对应的数据源的时候, 我们设置的数据源其实都是Druid的数据源, 并且其配置有两种方式, 当然这两种方式都需要我们导入对应的有关 德鲁伊 的依赖才行 一种是直接在开始设置为 druid 数据源类型的一种是在对应的正常的数据库配置下, 设置…

51 USART数据收发

1.0 USART实现单个数据收发 串口启动之前需要对串口进行初始化&#xff0c;主要是设置产生波特率的定时器1&#xff0c;使用串口的工作方式还是中断的工作方式具体的配置步骤如下所示。 注&#xff1a; 1&#xff1a; 确定TMOD &#xff08;定时器模式寄存器&#xff09; 确…

Thinkpad产品系列进BIOS设置(重装系统)

Thinkpad产品系列进BIOS设置&#xff08;重装系统&#xff09; 对于大多数ThinkPad笔记本产品&#xff08;T、X、W、P、L、E系列部分除外&#xff09;&#xff0c;例如T14、T15、T490、T590、X13、X390等&#xff0c;您需要在启动计算机时&#xff0c;当显示ThinkPad徽标时&…

【简单讲解Perl语言】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

46【Aseprite 作图】发光

1 通过“编辑 - 特效 - 卷积矩阵”&#xff0c;这次选择“7*7”&#xff0c;可以做出窗户的效果