当前位置: 首页 > article >正文

并发编程 - 线程同步(二)

经过前面对线程同步初步了解,相信大家对线程同步已经有了整体概念,今天我们就来一起看看线程同步的具体方案。

在这里插入图片描述

01、ThreadStatic

严格意义上来说这两个并不是实现线程同步方案,而是解决多线程资源安全问题,而我们研究线程同步最终也是为了解决多线程资源安全问题,因此就先说下这两个用法。

ThreadStatic特性可以实现线程本地存储,使得每个线程都有一个独立的字段副本。从而避免不同线程间共享资源。

使用ThreadStatic时需要注意以下几点:

1、ThreadStatic仅能作用于静态字段;。

2、ThreadStatic字段不应使用内联初始化。

3、每个线程都会有独立的_threadLocalVariable实例,当线程退出时,相关的线程本地存储会被清除。

4、由于 ThreadStatic 是线程局部存储,它并不是跨线程共享数据的解决方案。

使用起来也很简单,我们来着重说说上面注意点的第二点,虽然语法上可以写出内联初始化,但是这样会导致一个问题:仅有访问其的首个线程上可以获取其初始化变量值,而其他所有线程都只能获取到变量类型的默认值。比如下面这段代码:

[ThreadStatic]
public static int _threadStaticValue = 1;
public static void ThreadStaticRun()
{
    var thread1 = new Thread(ThreadStatic1);
    var thread2 = new Thread(ThreadStatic2);
    var thread3 = new Thread(ThreadStatic3);
    thread1.Start();
    thread2.Start();
    thread3.Start();
}
static void ThreadStatic1()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic2()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic3()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}

也就是上面代码只有一个线程能打印出1,其他线程都只能打印出0,我们看看实际打印结果:

在这里插入图片描述

因此注意项第二点提出ThreadStatic字段不应使用内联初始化,因为这样并不能保证每个线程都能获取到相同的初始值。

也因为ThreadStatic有这个缺陷所以引出了ThreadLocal。

02、ThreadLocal

可以说ThreadLocal功能和ThreadStatic完全一样,并且还解决了其缺陷,因此更推荐使用ThreadLocal。

可以使用 System.Threading.ThreadLocal 类型创建一个基于实例的线程本地变量,该变量由你提供的 Action 委托在所有线程上进行初始化。如下示例中,访问_threadLocalValue的所有线程都可以获取到初始化值1。

private static ThreadLocal<int> _threadLocalValue = new ThreadLocal<int>(() => 1);
public static void ThreadLocalRun()
{
    var thread1 = new Thread(ThreadLocal1);
    var thread2 = new Thread(ThreadLocal2);
    var thread3 = new Thread(ThreadLocal3);
    thread1.Start();
    thread2.Start();
    thread3.Start();
}
static void ThreadLocal1()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
}
static void ThreadLocal2()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
}
static void ThreadLocal3()
{
    Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue.Value}");
}

执行结果如下:

在这里插入图片描述

并且可以通过ThreadLocal.Value 属性进行读取和写入,也就是通过_threadLocalValue.Value对变量进行赋值和取值。

03、volatile关键字

首先volatile关键字同样不是一个完整的线程同步机制,其主要作用是防止缓存和防止编译器优化。

在C#语言开发中,由于编译器优化、JIT 编译、硬件缓存以及内存重排序等行为,很容易使得程序出现并发错误,尤其在多线程环境下这些情况会更为明显。虽然这些优化是在不影响程序逻辑的情况下进行的,但是因为重新排序对内存的读取和写入,进而可能导致数据竞争和同步问题。

volatile关键字就是为了告诉编译器和运行时:该字段的值可能会被多个线程同时修改,因此每次访问该字段时,都应该直接从主内存中读取,而不是使用寄存器或缓存中的值。这样可以防止 CPU 的优化行为导致某些线程读取到过时的值。

我们一起看看如下代码:

//控制线程的标志
private static bool _flag = false;
//计数器
private static int _counter = 0;
public static void VolatileRun()
{
    var thread1 = new Thread(Volatile1);
    var thread2 = new Thread(Volatile2);
    thread1.Start();
    thread2.Start();
    thread1.Join();
    thread2.Join();
    //Console.WriteLine($"计数器最后的值: {counter}");
}
static void Volatile1()
{
    //注意:以下两行代码可能按相反的顺序执行
    //设置计数器
    _counter = 88;
    //线程1:设置标志位,并且增加计数器
    _flag = true;
}
static void Volatile2()
{
    //注意:_counter可能优先于_flag读取
    //线程2:等待标志位变为 true,然后读取计数器
    //等待 _flag 被设置为 true
    while (!_flag) ;
    //打印计数器值
    Console.WriteLine($"当前计数器的值: {_counter}");
}

上面的代码很难在复现下面要说的问题,因此下面仅以此代码作为示例讲解。

上面代码的问题在于,经过编译器优化和内存重排序后, Volatile1线程中的两行赋值代码可能被颠倒了顺序,如果从单线程角度来说这个顺序颠倒无关紧要,最总结果都是_counter被赋值了88,_flag被赋值了true。但是在多线程环境下,对于Volatile2线程来说就完全不一样了,此时却先读取到_flag为true,然后打印_counter为0,和预期完全不一样。

我们再从另一个角度来说,假定Volatile1线程中的代码安装编码顺序执行了,没有被优化。在编译Volatile2线程中的代码时,编译器必须生成代码将_flag和_counter从RAM(主存)中读入CPU寄存器,此时RAM可能先读入_counter的值,为0。与此同时Volatile1线程可能执行,将_counter修改为88,想_flag修改为true。此时Volatile2线程的CPU寄存器还没有看到_counter已被Volatile1线程修改为88,然后继续将_flag的值从RAM中读入CPU寄存器,但是由于此时_flag已经被Volatile1线程修改为true,所以最后Volatile2线程同样会打印_counter为0。

开发时很容易忽略这些细微之处,并且由于开发调试环境不会进行代码优化,就导致问题往往到了生产环境下才显现出来。

为了解决这个问题我们就可以使用volatile关键字了。对于被声明为volatile的字段将从编译器优化、JIT 编译、硬件缓存以及内存重排序等优化中排除,使用也很简单,可以如下使用:

private static volatile bool _flag = false;

另外volatile关键字不能引用于double,long,数组等类型,可以使用Volatile.Read和Volatile.Write静态方法来完成。

同时volatile关键字虽然可以解决许多并发问题,但是因为其不是原子操作,因此它并不能算是一个完整的线程同步机制,因此在多线程环境下还是需要借助一些其他同步机制来保证线程安全。

因此volatile最大的应用场景就是在需要保证多个线程访问同一个共享变量时,大家都可以立刻看到最新的值,尤其是不涉及复杂操作如递增递减等。

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


http://www.kler.cn/a/525294.html

相关文章:

  • QPS 值是怎样进行计算和应用的
  • 关于产品和技术架构的思索
  • CMAKE工程编译好后自动把可执行文件传输到远程开发板
  • 16届蓝桥杯寒假刷题营】第2期DAY5IOI赛
  • 【javaweb项目idea版】蛋糕商城(可复用成其他商城项目)
  • 面试经典150题——图
  • 【2024年华为OD机试】 (A卷,200分)- 服务中心选址(JavaScriptJava PythonC/C++)
  • Python异步编程核武器:asyncio.gather() 的终极使用手册
  • 使用scikit-learn中的KNN包实现对鸢尾花数据集或者自定义数据集的的预测。
  • SpringBoot+Vue的理解(含axios/ajax)-前后端交互前端篇
  • 【开源免费】基于SpringBoot+Vue.JS社区智慧养老监护管理平台(JAVA毕业设计)
  • gif动画图像优化,相同的图在第2,4,6帧中重复出现,会增加图像体积吗?
  • 迭代推理机制提升AI精准性
  • 一文简单回顾Java中的String、StringBuilder、StringBuffer
  • 【阅读笔记】基于图像灰度梯度最大值累加的清晰度评价算子
  • Python里的小整数问题挺有意思的
  • 【NLP251】NLP RNN 系列网络
  • pytorch线性回归模型预测房价例子
  • 乐优商城项目总结
  • AI大模型开发原理篇-3:词向量和词嵌入
  • Ubuntu 16.04安装Lua
  • Yii框架中的正则表达式:如何实现高效的文本操作
  • 【Unity教程】零基础带你从小白到超神part3
  • 观察者模式和订阅发布模式的关系
  • 03链表+栈+队列(D1_链表(D1_基础学习))
  • hdfs之读写流程