上一章我们学习了原子操作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. 我和JVM初次约会

    前言:该篇是参考结合<高手深度解析:JVM是什么>,自己剔除添加了一些内容整理的而来,这里感谢<高手深度解析:JVM是什么>的文章的指点讲解. JVM的生命周期 "J ...

  2. Java Study For Six Day( 面向对象二)

    static(静态)关键字 用于修饰成员(成员变量和成员函数) 被修饰后的成员具备以下的特点 随着类的加载而加载 优先于对象存在 被所有的对象共享 可以被类名直接调用 静态注意事项 静态方法只能访问静 ...

  3. 从Hbase shell理解列式存储

    列存储和行存储在理解上的差别挺大,特别是在非常数据行存储之后. 在行存储中,每张表的结构是固定的,某一列可以没有值但是这一列是必须在的.那么可以理解行存储的数据是结构化的. 但是列存储确有每行的数据却 ...

  4. Spring AOP基础、快速入门

    介绍 AOP,面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理.日志.缓存.权限控制.限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度.切面就是那些与业务 ...

  5. Fiddler对手机APP进行抓包

    1.下载安装Fiddler 2.打开Fiddler菜单项Tools-> Options->HTTPS 勾选CaptureHTTPS CONNECTs,点击Actions, 勾选Decryp ...

  6. 洛谷P7911 [CSP-J 2021] 网络连接题解

    普通的模拟题,数据很小,基本排除超时超空间的可能 上代码: #include<bits/stdc++.h> #define LL long long using namespace std ...

  7. 在腾讯云 EMR 上使用 GooseFS 加速大数据计算服务

    GooseFS 是腾讯云对象存储团队最新推出的高性能.高可用以及可弹性伸缩的分布式缓存系统,依靠对象存储(Cloud Object Storage,COS)作为数据湖存储底座的成本优势,为数据湖生态中 ...

  8. GienTech动态|入选软件和信息技术服务名牌企业;荣获城市数字化转型优秀案例;参加第四届深圳国际人工智能展

    ​ 中电金信入选"2023第二届软件和信息技术服务名牌企业" ​ 近日,中国电子信息行业联合会发布了"2023第二届软件和信息技术服务名牌企业"名单,中电金信入 ...

  9. JDK 18 最新动态和 JDK 19 新特性预测

    JDK 18 最新动态和 JDK 19 新特性预测_语言 & 开发_Michael Redlich_InfoQ精选文章 里面提到文章 定界延续(delimited continuations) ...

  10. MYSQL支持的数据类型-数值类型

    一.数值类型分类 MYSQL支持所有标准SQL中的数值类型,其中包括严格数值类型(INTEGER.SMALLINT.DECIMAL和NUMERIC),以及近似数值数据类型(FLOAT.REAL和DOU ...