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

news2024/11/26 18:20:39

简介

多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。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/1833562.html

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

相关文章

图书管理系统代码(Java)

1、运行演示 QQ2024528-205028-HD 详细讲解在这篇博客&#xff1a;JavaSE&#xff1a;图书管理系统-CSDN博客 2、所建的包 3、Java代码 3.1 book包 3.1.1 Book类代码 package book;/*** Created with IntelliJ IDEA.* Description:* User: dings* Date: 2024-05-13* Time:…

CentOS 7.9检测硬盘坏区、实物定位(三)

系列文章目录 CentOS 7.9上创建JBOD&#xff08;一&#xff09; CentOS 7.9上创建的JBOD阵列恢复&#xff08;二&#xff09; 文章目录 系列文章目录前言一、在系统中找到硬盘对应的盘符二、使用命令定位实物1.badblocks检测坏块2.对2T以上的硬盘检测&#xff08;对本篇非必要…

【Mongodb-01】Mongodb亿级数据性能测试和压测

mongodb数据性能测试 一&#xff0c;mongodb数据性能测试1&#xff0c;mongodb数据库创建和索引设置2&#xff0c;线程池批量方式插入数据3&#xff0c;一千万数据性能测试4&#xff0c;两千万数据性能测试5&#xff0c;五千万数据性能测试6&#xff0c;一亿条数据性能测试7&am…

力扣每日一题 6/17 枚举+双指针

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;IT竞赛 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 522.最长特殊序列II【中等】 题目&#xff1a; 给定字符串列表 strs &…

.Net OpenCVSharp生成灰度图和二值图

文章目录 前言一、灰度图二、二值图 前言 使用OpenCVSharp生成图片的灰度图和二值图 .Net 8.0版本&#xff0c;依赖OpenCvSharp4和OpenCvSharp4.runtime.win组件。 原图&#xff1a; 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、灰度图 /// &…

亚马逊跟卖ERP系统,怎样提升新手卖家选品的质量与效率!

今天给新手卖家推荐一款实用ERP系统&#xff0c;相信新手卖家在碰到最难的问题应该就是选品了吧&#xff0c;那我们该如何快速解决我们找品难找品慢的问题呢&#xff0c;现在有一套专门为跟卖打造的一款全功能erp系统&#xff0c;从我们选品上传&#xff0c;到我们采购&#xf…

针对微电网中可时移,柔性,基础负荷的电价响应模型---代码解析

前言&#xff1a; 在上两篇帖子中&#xff0c;讲解了我对于粒子群算法的理解&#xff0c;站在巨人的肩膀上去回望&#xff1a;科研前辈们确实非常牛逼&#xff0c;所以它才成为了非常经典的算法。这篇帖子主要是想分享一下&#xff0c;对于微电网、电力系统的论文中&#xff0c…

JavaScript事件类型和事件处理程序

● 之前我们用过了很多此的点击事件&#xff0c;这次让我们来学习另一种事件类型 mouseenter “mouseenter” 是一个鼠标事件类型&#xff0c;它在鼠标指针进入指定元素时触发。 const h1 document.querySelector(h1); h1.addEventListener(mouseenter, function (e) {aler…

用Python pillow 创建和保存GIF动画

使用pillow库来创建和保存gif GIFs:图形交换格式(gif)是一种位图图像格式&#xff0c;由美国计算机科学家Steve Wilhite于1987年6月15日领导的在线服务提供商CompuServe的一个团队开发。 一个GIF文件通常存储一个图像&#xff0c;但该格式允许在一个文件中存储多个图像。该格…

用Vue3和p5.js打造一个交互式数据可视化仪表盘

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 基于 Vue.js 集成 p5.js 实现交互式波形图 应用场景介绍 在数据可视化领域&#xff0c;波形图广泛应用于展示动态变化的数据&#xff0c;如声音信号、心跳曲线等。通过动态绘制波形图&#xff0c;用户可以直观…

25天录用!快到飞起的宝藏SSCI,免版面费,1天见刊!毕业评职即刻拿下

本周投稿推荐 SSCI • 中科院2区&#xff0c;6.0-7.0&#xff08;录用友好&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; CNKI • 7天录用-检索&#xff08;急录友好&#xff09; SCI&EI • 4区生物医学类&#xff0c;0.5-1.0&#xff08;录用…

关于禁止word的无用插入模式

这是我的word版本号 点击左上角文件选项 找到左侧最下方的选项 点击高级 把这两个叉掉

✅生产问题之Emoji表情如何操作存储,MySQL是否支持

针对 Emoji 表情 MySQL 存储是否支持的问题&#xff0c;结论是&#xff1a; MySQL 中可以存储 emoji 表情&#xff0c;但需要使用 UTF8MB4 字符编码。如果使用 UTF8MB3&#xff0c;存储这些扩展字符会导致解析错误。 课外补充 MySQL 对 Unicode 的支持 Unicode 字符集已成为…

整合第三方技术-整合JUnit

黑马程序员Spring Boot2 文章目录 名称&#xff1a;SpringBootTest类型&#xff1a;测试类注解位置&#xff1a;测试类定义上方作用&#xff1a;设置JUnit加载的SpringBoot启动类范例&#xff1a;

Stable-Baseline3 x SwanLab:可视化强化学习训练

Stable Baselines3 (SB3) 是一个强化学习的开源库&#xff0c;基于 PyTorch 框架构建。它是 Stable Baselines 项目的继任者&#xff0c;旨在提供一组可靠且经过良好测试的RL算法实现&#xff0c;便于研究和应用。StableBaseline3主要被应用于机器人控制、游戏AI、自动驾驶、金…

[面试题]Spring

[面试题]Java【基础】[面试题]Java【虚拟机】[面试题]Java【并发】[面试题]Java【集合】[面试题]MySQL[面试题]Maven[面试题]Spring Boot[面试题]Spring Cloud[面试题]Spring MVC[面试题]Spring[面试题]MyBatis Spring 是一个很庞大的技术体系&#xff0c;可以说包容一切&…

【PL理论】(22) 函数式语言:多参数 | 柯里化 (Currying) : 将多参数函数实现为返回一个函数的函数

💭 写在前面:本章我们将继续讲解函数式语言,介绍多参数,着重讲解柯里化的概念,将多参数函数实现为返回一个函数的函数。目录 0x00 多参数(Multiple Arguments) 0x01 柯里化(Currying) 0x00 多参数(Multiple Arguments) 我们可以定义一个带有多个参数的函数吗?比…

计算机组成原理之存储器(一)

文章目录 存储器概述存储器的分类情况按照存储器在系统中的作用分类按存储介质分类按存取方式分类 主存储器的技术指标 存储器概述 程序的局部性原理&#xff08;构成多级存储系统的依据&#xff09;&#xff1a;在某一个时间段你频繁访问某一局部的存储器地址空间&#xff0c;…

短剧系统搭建全攻略:功能齐全,一步到位

前言 近年来&#xff0c;短剧系统以其独特魅力&#xff0c;成为大众消遣娱乐的热门选择。简单来说&#xff0c;短剧系统就是用来看短剧的小程序&#xff0c;它汇集了丰富多彩的短剧资源&#xff0c;让观众随时随地享受观影乐趣。本文将为你详细解析短剧系统的搭建全攻略&#…

web前端之vue一键部署的shell脚本和它的点.bat文件、海螺AI、ChatGPT

MENU 前言vite.config.ts的配置deploy文件夹的其他内容remote.shpwd.txtdeploy.bat 前言 1、在src同级新建deploy.bat文件&#xff1b; 2、在src同级新建deploy文件夹&#xff0c;文件夹中新建pwd.txt和remote.sh文件&#xff1b; 3、配置好后&#xff0c;直接双击deploy.bat文…