并发编程 - 线程同步(四)之原子操作Interlocked详解一

上一章我们了解了原子操作Interlocked类的设计原理及简单介绍,今天我们将对Interlocked的使用进行详细讲解。

在此之前我们先学习一个概念------原子操作。

01、Read方法

该方法用于原子的读取64位值,有分别针对long类型和ulong类型的两个重载方法;

对于64位系统,64位数据类型的读取本身就是原子操作;而对于32位系统,64位数据类型的读取需要至少两个原子指令,因此在32位系统可以通过Read方法对64位数据类型进行原子读取。

用法也很简单,示例如下:

csharp 复制代码
private static long _readValue = 0;
public static void ReadRun()
{
    var thread = new Thread(ModifyReadValue);
    thread.Start();
    Thread.Sleep(100);
    var value = Interlocked.Read(ref _readValue);
    Console.WriteLine("原子读取long类型变量: " + value);
}
static void ModifyReadValue()
{
    _readValue = 88;
    Console.WriteLine("修改long类型变量: " + _readValue);
}

运行结果如下:

因为系统环境原因无法模拟出32位系统效果,因此这里只是给了个简单使用示例。

02、Increment方法

该方法用于原子的递增指定的变量,并返回递增后的新值。该方法有4个重载方法,分别为long、ulong、int和uint四种数据类型;该方法适用于多线程环境中需要安全递增变量的场景,如计数器、资源管理等。

对于加法操作,无论是i+1,还是i++或++i,都不是线程安全的,最终可能会生成3条CPU指令,整个操作过程大致如下:

1.将 i 的值加载到寄存器,即从内存中读取i;

2.将寄存器中值加1,即i值加1;

3.最后将寄存器中值回写到i,即完成i值的变更;

而在这编码层面为1行代码,而CPU层面为3行指令的操作中,随时都有可能被线程调度器打断,而导致其他线程同时对i进行操作,最终导致竞争条件,最后数据错乱。

下面我们来举个例子,启动100个线程,分别对一个共享变量进行1000次递增1,最后打印出共享变量,运行这个示例9次观察每次运行结果,代码如下:

csharp 复制代码
private static long _incrementValue = 0;
public static void IncrementRun()
{
    //运行9次测试,观察每次结果
    for (var i = 1; i < 10; i++)
    {
        //启动100个线程,对变量进行递增
        var threads = new Thread[100];
        for (var j = 0; j < threads.Length; j++)
        {
            threads[j] = new Thread(ModifyIncrementValue);
            threads[j].Start();
        }
        //等待所有线程执行完成
        foreach (var thread in threads)
        {
            thread.Join();
        }
        //最后打印结果
        Console.WriteLine($"第 {i} 运行结果: {_incrementValue}");
        _incrementValue = 0;
    }
}
static void ModifyIncrementValue()
{
    for (var i = 0; i < 1000; i++)
    {
        ++_incrementValue;
    }
}

先看下执行结果:

可以发现每次的运行结果都不相同,并且结果也不对。这就是因为++i操作并不是原子操作,是线程不安全的。

只需要把上面代码:

csharp 复制代码
++_incrementValue;

改为:

csharp 复制代码
Interlocked.Increment(ref _incrementValue);

即可解决上面的问题,修改过后,我们再来看看执行结果:

03、Decrement方法

该方法用于原子的递减指定的变量,并返回递减后的新值。该方法同样有4个重载方法,分别为long、ulong、int和uint四种数据类型;

该方法和Increment方法基本一样,区别就是一个是递增一个是递减,因此用法可以直接参考Increment方法,这里就不做详细讲解了。

04、Add方法

该方法用于原子的对两个变量求和,将第一个变量替换为两者和,并返回操作后第一个变量的新值。该方法同样有4个重载方法,分别为long、ulong、int和uint四种数据类型;

虽然这个方法叫求和是加法,但是只需要把第2个参数变为负数,既可以实现减法。简单来说该方法可以实现原子的对两个变量求和与求差。

上面Increment方法和Decrement方法,只能对变量每次进行递增递减1,而能随意加减,可以通过Add方法实现两个变量进行加减。

下面我们用代码实现累加和累减示例用来说明Add使用方法,就不展示线程安全差异了,可以参考Increment方法中的示例,自己写一个线程不安全的示例。

csharp 复制代码
private static long _addValue = 0;
public static void AddRun()
{
    for (var j = 0; j < 1000; j++)
    {
        //_addValue =_ addValue + j;
        Interlocked.Add(ref _addValue, j);
    }
    Console.WriteLine($"累加结果: {_addValue}");
    _addValue = 0;
    for (var j = 0; j < 1000; j++)
    {
        //_addValue =_ addValue - j;
        Interlocked.Add(ref _addValue, -j);
    }
    Console.WriteLine($"累减结果: {_addValue}");
}

执行结果如下:

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

相关推荐
yics.2 天前
多线程——单例模式
java·单例模式·多线程·线程安全
poemyang2 天前
从MESA模型到锁升级:synchronized性能逆袭的底层逻辑
java·并发编程·java并发编程
poemyang3 天前
一把锁的两种承诺:synchronized如何同时保证互斥与内存可见性?
多线程·并发编程·java并发编程
青草地溪水旁11 天前
pthread_mutex_lock函数深度解析
linux·多线程·pthread
失散1320 天前
并发编程——17 CPU缓存架构详解&高性能内存队列Disruptor实战
java·缓存·架构·并发编程
荣淘淘22 天前
互联网大厂求职面试记:谢飞机的搞笑答辩
java·jvm·spring·面试·springboot·线程池·多线程
牟同學23 天前
从竞态到原子:pread/pwrite 如何重塑高效文件 I/O?
linux·网络编程·c·多线程
荣淘淘24 天前
互联网大厂Java面试三大回合全解析:从语言特性到性能安全
java·安全·面试·性能优化·互联网·多线程·语言特性
失散1324 天前
并发编程——06 JUC并发同步工具类的应用实战
java·架构·并发编程
失散131 个月前
并发编程——11 并发容器(Map、List、Set)实战及其原理分析
java·架构·并发编程