Design principle: Immutability不可变性与对mutable变量的Synchronization方法

news2024/9/21 17:24:53

不可变性(Immutability)在设计模式中是指一个对象在创建后其状态就不能改变。这是一种编程思想和设计原则。在某些情况下,使用不变对象可以带来许多好处:

  1. 简化代码 make things very simple:不可变对象在创建后状态不会改变,因此无需考虑对象状态的变化,这使得代码更简单,更容易理解和维护。

  2. 线程安全 Inherently thread-safe:不可变对象是线程安全的,因为它们不会在多个线程之间共享可变状态。这消除了同步和锁定的需要,提高了性能和可靠性。

  3. 支持共享 No risks in sharing:不可变对象可以被多个客户端安全地共享,因为它们不会修改共享的状态。这可以节省内存和计算资源。

  4. 降低错误风险:由于不可变对象的状态在创建后就不会发生变化,因此可以减少因状态改变而导致的错误,提高系统的稳定性。

有以下几种方式来创建immutability的对象:

  • 将类的所有成员变量设为私有(private)和只读(final)。Make all fields final and private.
  • 在构造函数中初始化所有成员变量,并确保它们在对象创建后不会改变。Ensure that no methods may be overridden.
  • 不提供任何修改成员变量的方法(setter)。Don’t provide any mutators.
  • 如果类包含可变对象的引用,确保在返回这些引用时创建它们的副本,以避免客户端代码修改内部状态。Ensure security of any mutable components.

举个例子,下面是一个不具备immutable的代码:

public class Complex {
double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }
public double setRealPart(double re) { this.re = re; }
public double setImaginaryPart(double im) { this.im = im; }

进行immutable的修改后为:

public final class Complex {
private final double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// Getters without corresponding setters
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }

这里可能会有疑问,如果把set方法去掉了,怎么满足修改的需求呢?此时可以加一个修改方法只返回修改后的副本,以避免影响原来的对象,比如:

// subtract, multiply, divide similar to add
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}

同理,如果非得写set方法,只要返回a new copy of an object即可。

不变性(Immutability)的局限:

immutability只是一种设计思想,并不是唯一的标准,因为一方面写immutable的代码有时会导致额外的内存分配和垃圾回收开销,从而降低程序的性能;另一方面因为每次修改都需要创建一个新对象,这可能导致许多相似的对象同时存在于内存中。最重要的是,但在需要频繁修改对象的状态情况下,使用可变对象会更加简洁和高效(比如要记录一个人的银行账户,难道每次用户每次交易都新建一个账户对象吗?显然此时要做的是最小化mutable的部分,对于mutable的部分做好线程保护),就不要非得写immutable的代码了。

其他保实现程安全的方法

正如上面所说,能写immutable的部分就写immutable,但如果不得不需要变化,我们就通过一些其他方式来实现线程安全。

首先来看一个“线程不安全”的例子:

@NotThreadSafe
public class UnsafeSequence {
private int value;
public int getNext() {
return value++;
}
}

这段代码不是线程安全的,因为在getNext()方法中对value变量进行自增操作(value++)时可能发生竞争条件(race condition)。value++操作实际上包含了三个步骤:

  1. 读取value的当前值。
  2. value的值加1。
  3. 将新的值写回value

在多线程环境中,如果两个或多个线程同时执行getNext()方法,这些步骤可能会交错进行,导致value的更新丢失。

在这种情况下,我们明明通过两个线程分别给变量增加了1,应该一共加2,却最终只增加了1。对于这个特定的例子,无法仅通过使用immutability使其线程安全。这是因为UnsafeSequence类的主要目的是产生一个递增的序列,这意味着它需要在内部维护一个可变的状态(在这里是value变量)。不过,可以使用其他方法来使该代码线程安全,如同步或原子操作。 

synchronized关键字

synchronized关键字是Java中用于实现同步的一种机制,它用于确保在并发环境中,共享资源的访问和修改是互斥的。当一个线程正在执行一个被synchronized关键字修饰的方法或代码块时,其他线程必须等待,直到当前线程完成对共享资源的操作。这有助于防止多个线程同时访问和修改共享资源,从而避免竞争条件和数据不一致的问题。下面是使用这个关键字解决上面线程不安全的例子:

public class SafeSequence {
    private int value;

    public synchronized int getNext() {
        return value++;
    }
}

对于synchronization我们也会分Coarse-grained粗粒度同步(锁住整个对象)和fine-grained细粒度同步(锁住部分资源),取决于实际情况,锁住的东西越多往往对性能影响会越大(会变慢)。

AtomicInteger

另一种替代方案是使用java.util.concurrent.atomic包中的AtomicInteger类,它提供了原子操作,可以确保自增操作的原子性:

import java.util.concurrent.atomic.AtomicInteger;

public class SafeSequence {
    private AtomicInteger value = new AtomicInteger();

    public int getNext() {
        return value.getAndIncrement();
    }
}

volatile关键字

volatile关键字比synchronized功能上弱一点,它提供了一种同步机制,但它并不提供互斥访问。换句话说,使用volatile关键字可以确保变量的可见性,即当一个线程修改了volatile变量的值后,其他线程可以立即看到这个变化。然而,它并不能确保原子性,即在并发环境下,多个线程仍然可以同时访问和修改volatile变量,可能导致不一致的状态。没有synchronized强大。在这里volatile关键字并不能解决上面的线程不安全的问题,仅仅只是提出来做介绍。

private static volatile boolean stopRequested;

Thread Confinement线程封闭

刚刚讲的volatilesynchronized是用于处理在多个线程间共享变量时的同步问题,而线程封闭则是直接通过避免在多个线程间共享变量来消除同步问题,直接从根本上解决问题。

以下是实现线程封闭的一些方法:

  1. 使用局部变量Local variables:局部变量仅在声明它们的方法中可见,因此它们天然地属于拥有该方法的线程。当方法调用结束时,局部变量会从栈上移除,不会影响其他线程。

  2. 防御性拷贝defense copying:当你需要将一个对象从一个线程传递到另一个线程时,可以创建该对象的拷贝,这样每个线程都有自己的副本,避免了多线程访问同一对象的问题。(这其实又回到了immutability上了。)

  3. 使用ThreadLocal(针对Java):ThreadLocal是一个特殊的Java类,它允许你为每个线程存储一个单独的值。当你需要在线程之间共享数据时,可以使用ThreadLocal来确保每个线程都有自己的私有副本。

  4. 适应其他编程语言的特性:

    • JavaScript:由于JavaScript在单线程环境中运行,不需要考虑线程封闭问题。
    • Python:Python在多线程和多进程之间进行了明确的区分。多进程无法共享状态,除非通过特殊对象来实现。

通过使用这些方法就可以实线线程封闭的效果,但可以发现,只要实现了immutability就大大帮助实现线程封闭了,Immutability Simplifies Thread Confinement!由于不可变对象的状态不会改变,因此它们在多线程环境中是天然线程安全的。一旦一个对象被创建,任何线程都可以安全地访问它,而无需担心其他线程对其进行修改。

小结:

这篇文章首先讲了immutability的优缺点和实现例子, 它的其中一个优点就是线程安全。但对于不适合写成immutable的变量来说,为了实现线程安全我们也可以采用线程封闭或者是通过如synchronized的关键字或其他同步机制(如显式锁)来确保对共享资源的访问是原子的和有序的。

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

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

相关文章

【问题排查篇】一次业务问题对 ES 的 cardinality 原理探究 | 京东云技术团队

作者:京东科技 王长春 业务问题 小编工作中负责业务的一个服务端系统,使用了 Elasticsearch 服务做数据存储,业务运营人员反馈,用户在使用该产品时发现,用户后台统计的订单笔数和导出的订单笔数不一致! …

21.动态组件 component与keep-alive

目录 1 基本使用 2 按钮切换组件 3 keep-alive 3.1 在切换组件的时候,默认情况下被切换的组件就会被销毁 3.1.1 数据情况 3.1.2 生命周期函数情况 3.2 使用 keep-alive 3.3 keep-alive的生命周期函数 3.4 缓存指定组件 include 3.5 不缓存指定组…

【51单片机】点亮一个LED灯(看开发板原理图十分重要)

🎊专栏【51单片机】 🍔喜欢的诗句:更喜岷山千里雪 三军过后尽开颜。 🎆音乐分享【The Right Path】 🥰大一同学小吉,欢迎并且感谢大家指出我的问题🥰 目录 🍔基础内容 &#x1f3f3…

HMS Core 6.10.0版本发布公告

分析服务 ◆ 事件分析下新增商品订阅分析报告,帮助开发者了解应用内用户付费订阅概况,评估订阅付费价值; ◆ 营销分析、用户质量、转化分析以及过滤器中,新增广告系列/广告任务通过ID进行搜索的功能,通过更便捷高效的…

【笔试强训选择题】Day8.习题(错题)解析

作者简介:大家好,我是未央; 博客首页:未央.303 系列专栏:笔试强训选择题 每日一句:人的一生,可以有所作为的时机只有一次,那就是现在!!! 文章目录…

Skywalking

Skywalking skywalking是一个apm系统,包含监控,追踪,并拥有故障诊断能力的 分布式系统 一、Skywalking介绍 1.什么是SkyWalking Skywalking是由国内开源爱好者吴晟开源并提交到Apache孵化器的产品,它同时吸收了Zipkin /Pinpoint …

C语言初阶之常量和变量

常量和变量 什么是常量和变量常量示例 定义变量的方法变量的命名变量的分类示例 变量的使用变量的作用域和生命周期作用域生命周期 结语 什么是常量和变量 在C程序执行过程中,其值不发生改变的量称为常量,其值可变的量称为变量。它们可与数据类型结合起…

BoldReports Embedded Reporting 5.1 Crack

Embed Paginated Reports 嵌入式报告平台,商业智能报告解决方案,探索满足各种业务需求的报告功能,所有功能View Features,像素完美报告,创建像素完美的分页业务报表,随处部署,选择适合您的部署环…

Android---ANR问题分析

目录 ANR 概念 超时检测机制 如何避免 ANR 问题? ANR 分析 ANR 问题线上监控 ANR 概念 ANR(Application Not Response),是指应用程序未响应,Android 系统对于一些事情需要在一定时间范围内完成,如果超过预定时间未能得到有效…

VSCode作业1:猜数字游戏和简单计数器(包含完整代码)

目录 猜数字游戏 一、使用‘random’函数获取随机数 二、 分情况讨论输入值大小情况 三、HTML代码 四、CSS样式及运行效果 简单计数器(计时器) 一、使用‘setInterval’函数实现计数效果 二、使用’clearInterval‘函数实现暂停计数和重新计数效果 …

【机器学习】sklearn的集成学习用于图像分类从0到1,注意点和坑点

文章目录 前言1.需求分析1.1 场景1.2 解决方案 2. 代码2.1 提取特征2.2 构建分类器2.4 集成模型2.5 总的训练代码 3.fast api 封装4.总结 前言 深度学习崛起后,好像机器学习就没落了,但在固定场景下,还是很好用的。下面就是展厅项目的识别任…

解决Edge Dev更新后NewBing侧边栏消失的问题,并使用NewBing作画

文章目录 解决Edge Dev更新后NewBing侧边栏消失的问题,并使用NewBing作画问题来源操作步骤打开侧边栏步骤尝试让NewBing给出图像输出表情包或者其他图片使用NewBing作画 查看聊天记录插件 总结 解决Edge Dev更新后NewBing侧边栏消失的问题,并使用NewBing…

标签制作软件如何批量制作DotCode码

DotCode码是由不连续的点组成的二维条形码符号。设计的初衷是工业流水线上使用高速喷墨/激光打印机印刷产品有效期、批号以及序列号等。其尺寸是灵活可变的,可以根据货品表面的大小来调整印刷。下面带大家一起看一下在标签制作软件中如何批量制作: 打开…

STM32F4_十进制和BCD码的转换

目录 前言 1. BCD码 2. BCD码和十进制转换的算法 前言 最近在学习STM32单片机(不仅仅是32)的RTC实时时钟系统的过程中,需要配置时钟的时间、日期;这些都需要实现BCD码和十进制之间进行转换。这里和大家一起学习BCD码和十进制之…

C++函数必备简单知识

目录 1、函数的定义与声明 (1)定义 (2)声明 2、指针传参 3、引用 4、函数的引用传参 5、函数重载 overlord (1)参数数量不同 (2)参数类型不同 6、避免overlord歧义 7、内…

Opencv+Python图像像素处理

目录 二值图像的像素访问、修改 单个像素访问、修改 多个像素修改 彩色图像(三维数组) 像素访问、修改 BGR模式 像素访问、修改 二值图像的像素访问、修改 单个像素访问、修改 import numpy as np import cv2 as cv # 使用Numpy库中的函数zeros()可…

springboot登录验证

案例-登录认证 已经实现了部门管理、员工管理的基本功能,但是大家会发现,但没有登录,就直接访问到了Tlias智能学习辅助系统的后台。 这是不安全的,今天的主题就是登录认证。 最终要实现的效果就是用户必须登录之后,才…

Spark学习笔记【shuffle】

本文基本上是大数据处理框架Apache Spark设计与实现的Shuffle部分的学习。以及Spark基础知识Bambrow Shuffle解决啥问题 上游和下游,不同stage,不同的task之间是如何传递数据的。ShuffleManager管理ShuffleWrite和ShuffleRead 分为两个阶段&#xff1…

基于JavaWeb实现的寻码网文章资讯管理系统

一、技术结构 前端:html ajax 后端:SpringBootMybatis-plus 环境:JDK1.8 | Mysql | Maven | Redis 二、功能简介 数据库与代码截图 后端管理-登录页 后端管理-首页 后端管理-文章管理-发布文章 后端管理-文章管理-文章列表 后端管理-文…

Vue快速入门,常用指令,生命周期

Vue常用指令 案例&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"…