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.rhkb.cn/news/352590.html

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

相关文章

Vue 3与ESLint、Prettier:构建规范化的前端开发环境

title: Vue 3与ESLint、Prettier&#xff1a;构建规范化的前端开发环境 date: 2024/6/11 updated: 2024/6/11 publisher: cmdragon excerpt: 这篇文章介绍了如何在Vue 3项目中配置ESLint和Prettier以统一代码风格&#xff0c;实现代码规范性与可读性的提升。通过设置规则、解…

QT——MySQL数据库联用

一、ODBC 1、ODBC简介 ODBC全称为Open Database Connectivity,是一种用于数据库操作的标准接口。要使用ODBC,首先需要安装相应的ODBC驱动程序,然后在系统中配置ODBC数据源。接着,可以通过编程语言(如C++、Java等)或者数据库工具(如SQL Server Management Studio)来连…

【离散化 二维差分】850. 矩形面积 II

本文涉及知识点 离散化 二维差分 LeetCode850. 矩形面积 II 给你一个轴对齐的二维数组 rectangles 。 对于 rectangle[i] [x1, y1, x2, y2]&#xff0c;其中&#xff08;x1&#xff0c;y1&#xff09;是矩形 i 左下角的坐标&#xff0c; (xi1, yi1) 是该矩形 左下角 的坐标…

第十七课,海龟画图习题课(一)

图案一&#xff0c;半圆 import turtleturtle.circle(50, 180)turtle.left(90)turtle.forward(100) 图案二&#xff0c;同心圆 import turtleturtle.circle(100)turtle.right(90)turtle.penup()turtle.forward(50)turtle.pendown()turtle.left(90)turtle.circle(150) 图案三&am…

上海亚商投顾:沪指缩量调整 PCB概念股持续爆发

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 大小指数昨日走势分化&#xff0c;沪指全天震荡调整&#xff0c;创业板指午后涨超1%。消费电子板块全天强势&a…

持续学习的综述: 理论、方法与应用

摘要 为了应对现实世界的动态&#xff0c;智能系统需要在其整个生命周期中增量地获取、更新、积累和利用知识。这种能力被称为持续学习&#xff0c;为人工智能系统自适应发展提供了基础。从一般意义上讲&#xff0c;持续学习明显受到灾难性遗忘的限制&#xff0c;在这种情况下…

新手如何入门Web3?

一、什么是Web3&#xff1f; Web3是指下一代互联网&#xff0c;它基于区块链技术&#xff0c;致力于将各种在线活动变得更加安全、透明和去中心化。Web3是一个广义的概念&#xff0c;涵盖了包括数字货币、去中心化应用、智能合约等在内的多个方面。它的主要特点包括去中心化、…

深度学习 --- stanford cs231学习笔记四(神经网络的几大重要组成部分)

训练神经网络1 1&#xff0c;激活函数&#xff08;activation functions&#xff09; 激活函数是神经网络之于线性分类器的最大进步&#xff0c;最大贡献&#xff0c;即&#xff0c;引入了非线性。 1&#xff0c;1 Sigmoid sigmoid函数的性质&#xff1a; 结合指数函数的图像可…

element-plus的Tour 漫游式引导怎么去绑定Cascader 级联选择器

首先官方例子是用的button 官方.$el这个log出来是&#xff1a; 知道是以元素为准就拿对应的元素就行 级联选择器.$el是这样的&#xff1a; 你可以移入这个元素部分去看看是哪个要用的&#xff08;好像火狐直接放上去就可以看到元素表示&#xff0c;谷歌要双击或者右键选择去看…

[深度学习]--分类问题的排查错误的流程

原因复现&#xff1a; 原生的.pt 好使&#xff0c; 转化后的 CoreML不好使&#xff0c; 分类有问题。 yolov8 格式的支持情况 Format Argument Suffix CPU GPU 0 PyTorch - .pt True True 1 Tor…

C#——析构函数详情

析构函数 C# 中的析构函数&#xff08;也被称作“终结器”&#xff09;同样是类中的一个特殊成员函数&#xff0c;主要用于在垃圾回收器回收类实例时执行一些必要的清理操作。 析构函数: 当一个对象被释放的时候执行 C# 中的析构函数具有以下特点&#xff1a; * 析构函数只…

C语言 | Leetcode C语言题解之第162题寻找峰值

题目&#xff1a; 题解&#xff1a; int findPeakElement(int* nums, int numsSize) {int ls_max0;for(int i1;i<numsSize;i){if(nums[ls_max]>nums[i]);else{ls_maxi;}}return ls_max; }

Pycharm怎么默认终端连接远程服务器

因为经常需要从宿舍到学校内通勤&#xff0c;期间所有连接都会中断&#xff0c;所以每次开SSH特别麻烦&#xff0c;每次终端自动切换到本地&#xff1a; 每次都得点一下Start SSH Session 想要默认终端连接远程服务器&#xff0c;需要点File->Setting->Tools->SSH T…

无代码爬虫软件八爪鱼采集器-如何设计判断是、否

我们在设计采集规则的时候&#xff0c;可能会需要判断&#xff0c;比如采集评论的时候“展开更多回复”&#xff0c;就点击这个按钮&#xff0c;像这种情况就可以设计一个判断模块进入 判断模块添加后会自动生成两个&#xff0c;默认都是不判断直接执行&#xff0c;如果我们需要…

ROS实验课(三)

write in advance 此次实验课给我的生活来了沉重的一击&#xff0c;不单单是因为没有做出来&#xff0c;还因为我卡在了 插件 缺失 而无法解决。之前对待实验课&#xff0c;能在操作流程之外有暇思考具体的实现&#xff0c;此次只能记录简单的操作流程部分。 老规矩&#xff…

保姆级pycharm远程连接linux服务器

1、登录服务器&#xff0c;创建账号。 一般都是管理员账户登录&#xff0c;创建自己的账号。如果不需要&#xff0c;可跳过这步。 打开MobaXterm&#xff0c;点击左上角Session创建会话。 再点击左上角SSH&#xff0c;分别输入服务器ip和账号&#xff0c;最后点ok&#xff0c;进…

cloud_enum:一款针对不同平台云环境安全的OSINT工具

关于cloud_enum cloud_enum是一款功能强大的云环境安全OSINT工具&#xff0c;该工具支持AWS、Azure和Google Cloud三种不同的云环境&#xff0c;旨在帮助广大研究人员枚举目标云环境中的公共资源&#xff0c;并尝试寻找其中潜在的安全威胁。 功能介绍 当前版本的cloud_enum支…

Unity2D游戏制作入门 | 13 ( 之人物三段攻击 )

上期链接&#xff1a;Unity2D游戏制作入门 | 12(之人物受伤和死亡的逻辑动画)-CSDN博客 上期我们聊了人物的受伤和死亡的逻辑和动画&#xff0c;我们主要学习了事件的执行&#xff0c;即我们在人物受伤时可能会触发很多的事件&#xff0c;比如触发人物受伤的动画以及播放音乐等…

MinIO Enterprise Cache:实现超性能的分布式 DRAM 缓存

随着计算世界的发展和 DRAM 价格的暴跌&#xff0c;我们发现服务器配置通常配备 500GB 或更多的 DRAM。当您处理大型部署时&#xff0c;即使是那些具有超高密度 NVMe 驱动器的部署&#xff0c;这些服务器上的服务器数量乘以 DRAM 也会迅速增加&#xff0c;通常达到几 TB。该 DR…

【电脑日常问题】关于“已经安装了该产品的另一个版本”解决方法

问题描述: 在安装应用或者某游戏时弹出该窗口,虽然游戏已经安装完成,但是游戏无法正常运行。 原因分析: 出现这个问题,大概率可能是某次游戏安装完成,点击开始游戏的时候会自动安装一个软件,但是点了取消,以致于这个程序安装中断,但是已安装的部分还残留,目前是P社的…