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

简介

多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。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() {// 将数据库表中的数据加载到内存,存为Mapthis.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 {@Overridepublic 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.mzph.cn/diannao/30529.shtml

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

相关文章

20240619在飞凌OK3588-C的Linux R4系统下查找MIPI YUV摄像头的csi size err

20240619在飞凌OK3588-C的Linux R4系统下查找MIPI YUV摄像头的csi size err 2024/6/19 14:00 缘起&#xff0c;公司使用LVDS OUT的机芯&#xff0c;4LANE的LVDS输出。1920x108030分辨率&#xff08;1080p/30&#xff09; 通过FPGA转换为2LANE的MIPI OUT之后进RK3588/OK3588-C。…

sqlite3指令操作-linux

1.查看当前数据库位置 2.查看当前数据库文件下有哪些表 3.显示 某表创建时的SQL语句 4.打开、关闭显示列标题&#xff1b; 5.列对齐显示 6.列以‘&#xff0c;’分隔显示 .separator 7.查询表信息 8.插入消息 9.删除某一行内容 10.修改某行某列内容 11.修改表名字 alter tab…

浅谈golang字符编码

1、 Golang 字符编码 Golang 的代码是由 Unicode 字符组成的&#xff0c;并由 Unicode 编码规范中的 UTF-8 编码格式进行编码并存储。 Unicode 是编码字符集&#xff0c;囊括了当今世界使用的全部语言和符号的字符。有三种编码形式&#xff1a;UTF-8&#xff0c;UTF-16&#…

2024年项目进度控制软件大比拼:找出适合您团队的最佳工具

本文整理了9大热门项目进度控制软件&#xff1a;PingCode、Worktile、Monday.com、Asana、Trello、Jira、ClickUp、Wrike、Zoho Projects。并且进行详细介绍对比。 在项目管理工具的选择上&#xff0c;不同规模的团队有着各自的需求和偏好。例如&#xff0c;小型团队倾向于选择…

新手搭建Magic-API

项目场景&#xff1a; 我本是一个前端和GIS开发工程师&#xff0c;但新单位并没有配置完整的开发团队&#xff0c;确切说目前只有我一个人做开发&#xff0c;那么肯定避免不了要研究下后端。最近有一个小程序要开发&#xff0c;管理平台我直接用的fastAdminthinkphp写完了页面…

终极版本的Typora上传到博客园和csdn

激活插件 下载网址是这个&#xff1a; https://codeload.github.com/obgnail/typora_plugin/zip/refs/tags/1.9.4 解压之后这样的&#xff1a; 解压之后将plugin&#xff0c;复制到自己的安装目录下的resources 点击安装即可&#xff1a; 更改配置文件 "dependencies&q…

XL5300 dTOF测距模块 加镜头后可达7.6米测距距离 ±4%测距精度

XL5300 直接飞行时间&#xff08;dToF&#xff09;传感器是一个整体方案dTOF 模组&#xff0c;应用设计简单。片内集成了单光子雪崩二极管&#xff08;SPAD&#xff09;接收阵列以及VCSEL激光发射器。利用自主研发的 SPAD 和独特的ToF 采集与处理技术&#xff0c;XL5300模块可实…

软件产品进行确认测试有什么好处?第三方软件测试机构分享

软件确认测试是一项旨在验证软件是否符合预期需求和规格的测试活动。通过确认测试&#xff0c;您可以确保软件的功能、性能和用户界面的符合程度&#xff0c;从而降低软件发布后出现问题的风险。 一、软件产品进行确认测试的好处   1、减少软件发布后修复问题的成本。通过及…

python 版本管理工具 pyenv-win 安装

一、下载 pyenv pyenv-win 使用 powershell 下载 Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./ins…

Vue59-全局事件总线:任意组件间通信

一、原理图 只是总结出的经验&#xff0c;不是新的API&#xff01; 二、x的要求&#xff1a; 1、保证x被所有组件看见&#xff1b; 2、x可以调用的到$on&#xff0c;才能绑定事件&#xff0c;还能调用到&#xff1a;$of&#xff0c; $emit&#xff1b; 三、x的创建&#xff…

机器学习课程复习——奇异值分解

1. 三种奇异值分解 奇异值分解(Singular Value Decomposition, SVD)包含了: 完全奇异值分解(Complete Singular Value Decomposition, CSVD)紧奇异值分解(Tight Singular Value Decomposition, TSVD)截断奇异值分解(Truncated Singular Value Decomposition, TSVD)no…

助力低空经济-eVTOL/无人机ADS-B航管应答机选型指南

一、低空经济概述 “低空经济”在今年全国两会首次写入政府工作报告。近日&#xff0c;工业和信息化部、科学技术部、财政部、中国民用航空局印发《通用航空装备创新应用实施方案&#xff08;2024—2030年&#xff09;》&#xff0c;提出到2030年&#xff0c;推动低空经济形成…

主窗体设计

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Python、QT与PyCharm配置完成后&#xff0c;接下来需要对快手爬票的主窗体进行设计&#xff0c;首先需要创建主窗体外层为&#xff08;红色框内&…

相交链表(Leetcode)

题目分析&#xff1a; . - 力扣&#xff08;LeetCode&#xff09; 相交链表&#xff1a;首先我想到的第一个思路是&#xff1a;如图可知&#xff0c;A和B链表存在长度差&#xff0c;从左边一起遍历链表不好找交点&#xff0c;那我们就从后面开始找&#xff0c;但是这是单链表&…

一个新的剪辑拼接图片和视频类APP在测试阶段需要测试内容,以iPhone APP为例:

1.UI参照原型图和设计稿 如有改动&#xff0c;需及时沟通 2.iPad转屏、不同iPhone和iPad机型测试 3.黑夜白天模式 2.各功能模块流程需要测试跑通 3.订阅支付模块 a. UI设计是否和设计稿一致 b.涉及订阅的位置都要测试 c.免费试用是否显示&#xff1b;试用结束后&#xff0c…

HDFS笔记

第1章 HDFS概述 1.1 HDFS产出背景及定义 1&#xff09;HDFS产生背景 随着数据量越来越大&#xff0c;在一个操作系统存不下所有的数据&#xff0c;那么就分配到更多的操作系统管理的磁盘中&#xff0c;但是不方便管理和维护&#xff0c;迫切需要一种系统来管理多台机器上的文…

typeScript debug 调试

以leetcode 20为例 0.首先编写代码 function isValid(s: string): boolean {let stack: string[] []for (let index 0; index < s.length; index) {let x: string s[index]debuggerswitch (x) {case (:stack.push())breakcase [:stack.push(])breakcase {:stack.push(})…

快速压缩前端项目

背景 作为前端开发工程师难免会遇到需要把项目压缩成压缩文件来传送的情况&#xff0c;这时候需要压缩软件进行压缩文件处理 问题 项目中的依赖包文件非常庞大&#xff0c;严重影响压缩速度&#xff0c;即使想先删除再压缩&#xff0c;删除文件也不会很快完成 解决 首先要安…

EXCELITAS电源维修TLX302高压电源维修

埃赛力达电源维修 EXCELITAS电源维修 海曼电源维修 高压电源维修 EXCELITAS高压电源维修故障包括&#xff1a;无输出&#xff0c;高压达不到&#xff0c;电流达不到标准&#xff0c;高压打火,高压线接头处太靠近铁壳部分。无光,风扇不转。保险丝断&#xff0c;可以强制发光,不…

Java——构造器(构造方法)和 this

一、什么是构造器 构造器&#xff08;Constructor&#xff09;是Java类的一种特殊方法&#xff0c;用于初始化对象的状态。构造器在创建对象时被调用&#xff0c;可以对对象的成员变量进行初始化。 我之前的文章《Java——类和对象-CSDN博客》中也提到了构造器。 二、构造器…