上一章我们学习了原子操作Interlocked类的几个常用方法,今天我们将继续学习该类的其他方法。

01、Exchange方法

该方法用于原子的将变量的值设置为新值,并返回变量的原始值。该方法共有14个重载方法,其中13个为常见的数据类型,有1个为泛型版本。

我们可以根据该方法可以原子更新一个变量并且可以同时获取该变量旧值这一特性,设计一个简单的锁机制,大致思路如下:

1.定义一个标志位,如果该标志位旧值为0表示当前线程获取锁,否则表示当前线程无法获取锁;

2.当线程获取锁后,可以进行业务处理,安全的处理非线程安全资源访问的代码;

3.当线程处理完业务后,释放锁,即更新标志位值为0,当前线程则退出锁,其他线程可以继续获取锁;

实现代码如下;

//0 表示未锁定,1 表示锁定
private static long _exchangeValue = 0;
public static void ExchangeRun()
{
var rnd = new Random();
//启动10个线程
var threads = new Thread[10];
for (var j = 0; j < threads.Length; j++)
{
threads[j] = new Thread(ModifyExchangeValue);
threads[j].Start();
//等待一段随机的时间后再开始启动另一个线程
Thread.Sleep(rnd.Next(0, 100));
}
}
static void ModifyExchangeValue()
{
//更新_exchangeValue为1,同时获取_exchangeValue旧值
var oldExchangeValue = Interlocked.Exchange(ref _exchangeValue, 1);
//如果旧值为0,表示该逻辑未被其他线程占用
if (0 == oldExchangeValue)
{
//当前线程开始锁定该代码块,其他线程无法进入
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 线程 进入锁");
//模拟一些工作
//这里可以实现安全的处理非线程安全资源访问的代码
Thread.Sleep(100);
//释放锁
Interlocked.Exchange(ref _exchangeValue, 0);
//当前线程释放完锁,其他线程可以进入该代码块
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 线程 退出锁");
}
else
{
Console.WriteLine($" {Thread.CurrentThread.ManagedThreadId} 线程 无法进入锁");
}
}

我们可以看看执行结果:

可以发现当一个线程获取锁后,其他线程则无法再获取该锁,只有当前线程处理完成业务退出改锁后,其他线程才可以继续获取该锁。

02、Exchange方法

该方法是Exchange方法的泛型版本,具体使用可以参考上一节。

03、CompareExchange方法

该方法用于原子的比较第一个变量和第三个变量是否相等,如果相等,则将第一个变量替换为第二个变量值,并返回第一个变量的原始值;该方法也有14个重载方法,其中13个为常见的数据类型,有1个为泛型版本。

可以理解为该方法比Exchange方法多了一个条件判断。因此该方法可以应用于更复杂的业务场景。

下面我们就使用该方法实现CAS(Compare and Swap)算法,并实现一个简单的版本控制功能。

所谓版本控制就是指使用一个版本号来表示对象的状态,每次更新该对象时,我们都希望确保只有当当前版本号与我们预期的版本号一致时才能执行更新操作。否则,说明在这期间有其他线程更新了该对象,我们需要放弃当前操作或者重试。

首先我们需要构建一个版本化数据类,该类中有两个字段用于分别用于存储数据和版本号;并提供两个方法,一个方法用于获取当前数据和版本号,一个方法用于通过版本号更新数据。具体实现代码如下:

//版本化数据
public class VersionedData<T>(T data)
{
private T _data = data;
private long _version = 0;
//获取当前数据和版本号
public (T Data, long Version) GetData()
{
return (_data, _version);
}
//基于版本号尝试更新数据
public bool TryUpdateData(T data, long expectedVersion)
{
//如果_version与预期版本号相同,
//则对预期版本号加1后再替换为_version,
//同时返回_version旧值
var oldVersion = Interlocked.CompareExchange(ref _version, expectedVersion + 1, expectedVersion);
//如果_version旧值与预期版本号相同
if (oldVersion == expectedVersion)
{
//则版本号匹配,更新数据
_data = data;
return true;
}
//否则版本号不匹配,更新失败
return false;
}
}

完成版本化类设计后,就可以使用了,我们模拟两个线程,同时获取当前版本化数据和版本号,然后同时再更新数据,具体代码如下:

public static void CompareExchangeRun()
{
var versionedData = new VersionedData<string>("初始化数据");
//线程 1 尝试更新数据
var thread1 = new Thread(ModifyCompareExchangeValue);
//线程 2 尝试更新数据
var thread2 = new Thread(ModifyCompareExchangeValue);
thread1.Start(versionedData);
thread2.Start(versionedData);
thread1.Join();
thread2.Join();
//最终结果
var (finalData, finalVersion) = versionedData.GetData();
Console.WriteLine($"最终数据为 [{finalData}], 最终版本号为 [{finalVersion}]");
}
static void ModifyCompareExchangeValue(object param)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var versionedData = (VersionedData<string>)param;
var (data, version) = versionedData.GetData();
Console.WriteLine($"线程 {threadId} : 当前数据为 [{data}], 当前版本号为 [{version}]");
Console.WriteLine("---------------------------------------------------");
var newData = $"线程 {threadId} 数据";
var success = versionedData.TryUpdateData(newData, version);
Console.WriteLine($"线程 {threadId} 更新数据: [{(success ? "成功" : "失败")}]");
Console.WriteLine($" 数据预期更新为:[{newData}]");
Console.WriteLine($" 版本号预期更新为:[{version + 1}]");
Console.WriteLine("---------------------------------------------------");
}

我们来看看执行结果:

通过结果可以发现只有1个线程更新成功了。

04、CompareExchange方法

该方法是CompareExchange方法的泛型版本,具体使用可以参考上一节。

05、And方法

该方法用于原子的对两个变量进行按位与操作,将第一个变量替换为操作结果,并返回第一个变量的原始值;该方法同样有4个重载方法,分别为long、ulong、int和uint四种数据类型;

主要还是用于多线程环境下,对共享变量进行安全的原子性按位与操作,避免并发修改时可能出现的数据不一致问题。

下面看一个简单的例子:

public static void AndRun()
{
var a = 10; // 二进制: 1010
var b = 5; // 二进制: 0101
var oldA = Interlocked.And(ref a, b);
//1010 & 0101 = 0000 = 0
Console.WriteLine("操作后的值: " + a);
Console.WriteLine("返回的结果: " + oldA);
}

看看执行结果;

可以理解为就是两个数进行按位与运算,并且可以原子的更新原值,并返回原始值。

06、Or方法

该方法用于原子的对两个变量进行按位或操作,将第一个变量替换为操作结果,并返回第一个变量的原始值;该方法也有4个重载方法,分别为long、ulong、int和uint四种数据类型;

具体使用可以参考And方法。

07、MemoryBarrier方法

该方法用于强制执行内存屏障,作用范围当前线程,无返回值。后面有机会我们再详细讲解。

08、MemoryBarrierProcessWide方法

该方法用于提供进程范围的内存屏障,确保任何 CPU 的读取和写入无法跨屏障移动。后面有机会我们再详细讲解。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

并发编程 - 线程同步(五)之原子操作Interlocked详解二的更多相关文章

  1. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  2. 33 - 并发编程-线程同步-Event-lock

    目录 1 线程同步 1.1 Event 1.1.1 什么是Flag? 1.1.2 Event原理 1.1.3 吃包子 1.2 Lock 1.2.1 lock方法 1.2.2 计数器 1.2.3 非阻塞 ...

  3. Java线程同步的四种方式详解(建议收藏)

    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...

  4. Java并发编程(07):Fork/Join框架机制详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.Fork/Join框架 Java提供Fork/Join框架用于并行执行任务,核心的思想就是将一个大任务切分成多个小任务,然后汇总每个小任务 ...

  5. java并发编程笔记(五)——线程安全策略

    java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...

  6. C#并行编程-线程同步原语

    菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...

  7. [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers

    本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...

  8. Java并发编程:同步容器

    Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...

  9. 【转】Java并发编程:同步容器

    为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...

  10. 8、Java并发编程:同步容器

    Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...

随机推荐

  1. Mac 上常见的环境配置文件

    当使用命令行终端进行开发时,环境配置文件可以用来自动化执行一些命令或设置环境变量,以提高工作效率和方便使用.在 Mac 上,常见的环境配置文件有以下几种: 1. 在.bash_profile文件中配置 ...

  2. L4168爱普生打印机重新刷机解决printer mode问题

    同事的一台EPSON L4168打印机出现无法打印的故障,显示printer mode.使用官网的升级工具,在更新升级模式下(用电源+左+下+三角形四个手指按住开启)刷机,重启问题依旧(仍然显示pri ...

  3. uniapp 画布

    1.前言 uniapp中的canvas与HTML中的canvas用法并不同,他的使用文档请参考微信小程序画布 2.基本使用 1.准备canvas容器,并为其设置canvas-id和宽高(为了兼容H5, ...

  4. solon 集成 kafka-clients

    使用 kafka-clients 原本是比较简单的事情.但有些同学习惯了 spring-kafka 后,对原始 java 接口会陌生些.会希望有个集成的示例. <dependency> & ...

  5. JDK 18 及以上使用标准输出流中文输出乱码问题

    著作权归作者所有. 商业转载请联系作者获得授权,非商业转载请注明出处. 链接:https://stazxr.cn/2024/12/05/JDK-18-以上使用标准输出流中文输出乱码问题/ 来源:終わり ...

  6. 鸿蒙UI开发快速入门 —— part05:组件的样式复用

    1. 为什么要样式复用? 如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,样式的复用就很有必要了. 为此,鸿蒙推出了可 ...

  7. LSTM学习三维轨迹的Python实现

    一.引言 长短期记忆网络(LSTM)是一种强大的递归神经网络(RNN),广泛应用于时间序列预测.自然语言处理等任务.在处理具有时间序列特征的数据时,LSTM通过引入记忆单元和门控机制,能够更有效地捕捉 ...

  8. 从混沌到秩序:Python的依赖管理工具分析

    Python 的依赖管理工具一直没有标准化,原因主要包括: 历史发展的随意性:Python发展早期对于依赖管理的重视程度不足,缺乏从一开始就进行统一规划和设计的意识 社区的分散性:Python社区庞大 ...

  9. Netty系列之Netty安全性

    1.1. 严峻的安全形势 1.1.1. OpenSSL Heart bleed漏洞 2014年上半年对网络安全影响最大的问题就是OpenSSL Heart bleed漏洞,来自Codenomicon和 ...

  10. Qt编写百度地图综合应用(在线+离线+区域)

    一.前言 在现在很多的应用系统中,会提供一个地图模块,地图相关的应用和app也是非常多,最广泛的应用就属于导航,地图基本上分在线的和离线的两种,在线的一般都是实时的,数据也是最新的,速度很快路线很准, ...