上一章我们学习了原子操作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. java公式解析器学习与开发(2)——前缀表达式

    释义 前缀表达式就是前序表达式. 前缀表达式就是不含括号的算术表达式,而且它是将运算符写在前面,操作数写在后面的表达式,为纪念其发明者波兰数学家Jan Lukasiewicz也称为"波兰式& ...

  2. ArrayList removeRange方法分析

    <ArrayList原码分析>一文中提到了"为什么removeRange(int fromIndex,int toIndex)是protected的?" 先给出remo ...

  3. Litctf2024-郑州轻工业大学第二届ctf-校内赛道wp

    战队:怎落笔都不对 最终成绩校内第4 MISC 1. 盯帧珍珠 打开文件发现是一个图片,放入 010 查看得文件头是 gif 格式 改为gif后缀得到一个GIF图,在下面这个网站分解,即可得到flag ...

  4. ChatGPT在功能测试用例生成方面的优势

    功能测试是软件测试的非常重要的分类,所有软件系统都要保证功能的正确性,而测试用例则是功能测试的重中之重.测试用例的编写是测试人员必须认真面对的一件耗时费力.枯燥乏味的工作.如何才能快速高效地编写测试用 ...

  5. MongoDB|TOMCAT定时切割日志文件的脚本

    MongoDB用过一段时间后,日志较大,需要定时进行日志切割. 一.切割bash: splitlogmongo.sh #!/bin/bash log_dir="/home/mongodb/l ...

  6. PM-如何优雅的抄袭代码?世上所有代码都是一大抄

    你借了我的思想,在我的思想上,发展出一套理好的思想. 你借了我的代码,在我的代码上,开发出一套理好的代码.   你们知道程序员最熟悉,最熟练,最常用的两个快捷键是哪两个吗?没错,估计你现在心中所想的就 ...

  7. 【Amadeus原创】Docker容器的备份与还原

    主要作用: 就是让配置好的容器,可以得到复用,后面用到得的时候就不需要重新配置. 其中涉及到的命令有: docker commit 将容器保存为镜像 docker save -o 将镜像备份为tar文 ...

  8. 钉钉机器人发送信息shell

    #钉钉机器人发送信息shell 可作为shell函数模块调用,用于监控警报.jenkins发版通知等 微信API官方文档 https://ding-doc.dingtalk.com/doc#/serv ...

  9. 【Python】【Pandas】将符合条件行的某列数值改为负数

    萌狼蓝天情景还原: 支付宝/微信导出的账单,不管支出还是收入都是正数. 我想把支出的金额改成负数,其他不变就这样. 解决办法 这里用到的是pandas.apply e--下面的写法虽然比较麻烦,但是 ...

  10. 执行docker ps时提示"dial unix /var/run/docker.sock: connect: permission denied"

    0. 创建docker用户组 sudo groupadd docker 1. 将当前用户加入docker组 # sudo gpasswd -a $USER docker $ sudo usermod ...