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

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

在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/961717.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

回顾:Maven的环境搭建

1、下载apache-maven-3.6.0 **网址:**http://maven.apache.org 然后解压到指定的文件夹&#xff08;记住文件路径&#xff09; 2、配置Maven环境 复制bin文件夹 的路径D:\JavaTool\apache-maven-3.6.0\bin 环境配置成功 3、检查是否配置成功 winR 输入cmd 命令行输入mvn -v…

DRF开发避坑指南01

在当今快速发展的Web开发领域&#xff0c;Django REST Framework&#xff08;DRF&#xff09;以其强大的功能和灵活性成为了众多开发者的首选。然而&#xff0c;错误的使用方法不仅会导致项目进度延误&#xff0c;还可能影响性能和安全性。本文将从我个人本身遇到的相关坑来给大…

DeepSeek R1:中国AI黑马的崛起与挑战

文章目录 技术突破&#xff1a;从零开始的推理能力进化DeepSeek R1-Zero&#xff1a;纯RL训练的“自我觉醒”DeepSeek R1&#xff1a;冷启动与多阶段训练的平衡之道 实验验证&#xff1a;推理能力的全方位跃升基准测试&#xff1a;超越顶尖闭源模型蒸馏技术&#xff1a;小模型的…

电路研究9.2.4——合宙Air780EP中MQTT 相关命令使用方法研究

之前研究了FTP命令&#xff0c;这次研究一下MQTT命令了。 16.14 使用方法举例 9.5.3 MQTT 应用指南 4G 模块支持 MQTT 和 MQTT SSl 协议&#xff0c; MQTT 应用的基本流程如下&#xff1a; 1、如果要支持 SSL&#xff0c;配置 SSL 参数2、通过 TCP 连接到 MQTT 服务器 3、发送 …

寻找旋转数组中的最小元素:C语言实现与分析

在算法与编程的世界里&#xff0c;经常会遇到各种有趣的问题。今天我们来探讨一个经典的题目&#xff1a;寻找旋转数组中的最小元素。我们将通过C语言代码实现&#xff0c;并详细分析其原理和实现细节。 题目描述 给定一个可能旋转过的递增排序数组&#xff0c;找到数组中的最小…

Object类(3)

大家好&#xff0c;今天继续给大家介绍一下object类中的方法&#xff0c;那么话不多说&#xff0c;来看。 hashcode()这个方法,帮我们算了一个具体的对象位置,这里面涉及到数据结构,简单认为它是个内存地址,然后调用Integer.toHexString ()将这个地址以16进制输出。 该方法是一…

Kafka 日志存储 — 磁盘存储

Kafka 依赖与磁盘来存储和缓存消息&#xff0c;采用文件追加的方式来写入消息。顺序写盘的速度快于随机写内存。 1 磁盘存储 除顺序写入外&#xff0c;Kafka中大量使用了页缓存、零拷贝等技术来进一步提升吞吐性能。 1.1 页缓存 页缓存是操作系统实现的一种磁盘缓存&#x…

基于SpringBoot的阳光幼儿园管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

什么是长短期记忆网络?

一、概念 长短期记忆网络&#xff08;Long Short-Term Memory, LSTM&#xff09;是一种特殊的循环神经网络&#xff08;RNN&#xff09;&#xff0c;旨在解决标准RNN在处理长序列时的梯度消失和梯度爆炸问题。LSTM通过引入三个门&#xff08;输入门、遗忘门和输出门&#xff09…

Unity游戏(Assault空对地打击)开发(1) 创建项目和选择插件

目录 前言 创建项目 插件导入 地形插件 前言 这是游戏开发第一篇&#xff0c;进行开发准备。 创作不易&#xff0c;欢迎支持。 我的编辑器布局是【Tall】&#xff0c;建议调整为该布局&#xff0c;如下。 创建项目 首先创建一个项目&#xff0c;过程略&#xff0c;名字请勿…

996引擎 - NPC-动态创建NPC

996引擎 - NPC-动态创建NPC 创建脚本服务端脚本客户端脚本添加自定义音效添加音效文件修改配置参考资料有个小问题,创建NPC时没有控制朝向的参数。所以。。。自己考虑怎么找补吧。 多重影分身 创建脚本 服务端脚本 Mir200\Envir\Market_Def\test\test001-3.lua -- NPC八门名…

如何看待 OpenAI 的12天“shipmas”发布计划?

openAI的“Shipmas”并非单纯的营销活动,而是在用户增长、技术创新和市场竞争中的综合布局和战略体现。 史上最寒酸的发布会?继十月马斯克在好莱坞电影城高调发布特斯拉三款最新产品(无人出租车、无人巴士、人形机器人)后,十二月,OpenAI CEO 奥特曼宣布 OpenAI 将连续12…

蓝桥杯模拟算法:蛇形方阵

P5731 【深基5.习6】蛇形方阵 - 洛谷 | 计算机科学教育新生态 我们只要定义两个方向向量数组&#xff0c;这种问题就可以迎刃而解了 比如我们是4的话&#xff0c;我们从左向右开始存&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4 到5的时候y就大于4了就是越界了&…

第31篇:Python开发进阶:数据可视化与前端集成

第31篇&#xff1a;数据可视化与前端集成 目录 数据可视化概述 什么是数据可视化数据可视化的重要性 Python中的数据可视化库 MatplotlibSeabornPlotlyBokehAltair 数据可视化的基本概念 图表类型设计原则交互性与动态性 与前端框架的集成 前端框架概述Flask与Django集成数据…

240. 搜索二维矩阵||

参考题解&#xff1a;https://leetcode.cn/problems/search-a-2d-matrix-ii/solutions/2361487/240-sou-suo-er-wei-ju-zhen-iitan-xin-qin-7mtf 将矩阵旋转45度&#xff0c;可以看作一个二叉搜索树。 假设以左下角元素为根结点&#xff0c; 当target比root大的时候&#xff…

maven的打包插件如何使用

默认的情况下&#xff0c;当直接执行maven项目的编译命令时&#xff0c;对于结果来说是不打第三方包的&#xff0c;只有一个单独的代码jar&#xff0c;想要打一个包含其他资源的完整包就需要用到maven编译插件&#xff0c;使用时分以下几种情况 第一种&#xff1a;当只是想单纯…

联想拯救者R720笔记本外接显示屏方法,显示屏是2K屏27英寸

晚上23点10分前下单&#xff0c;第二天上午显示屏送到&#xff0c;检查外包装没拆封过。这个屏幕左下方有几个按键&#xff0c;按一按就开屏幕、按一按就关闭屏幕&#xff0c;按一按方便节省时间&#xff0c;也支持阅读等模式。 显示屏是 &#xff1a;AOC 27英寸 2K高清 100Hz…

python:求解偏微分方程(PDEs)

1.偏微分方程基本知识 微分方程是指含有未知函数及其导数的关系式&#xff0c;偏微分方程是包含未知函数的偏导数&#xff08;偏微分&#xff09;的微分方程。 偏微分方程可以描述各种自然和工程现象&#xff0c;是构建科学、工程学和其他领域的数学模型主要手段。科学和工程中…

Deepseek技术浅析(二):大语言模型

DeepSeek 作为一家致力于人工智能技术研发的公司&#xff0c;其大语言模型&#xff08;LLM&#xff09;在架构创新、参数规模扩展以及训练方法优化等方面都达到了行业领先水平。 一、基于 Transformer 架构的创新 1.1 基础架构&#xff1a;Transformer 的回顾 Transformer 架…

13JavaWeb——SpringBootWeb之事务AOP

1. 事务管理 1.1 事务回顾 在数据库阶段我们已学习过事务了&#xff0c;我们讲到&#xff1a; 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位。事务会把所有的操作作为一个整体&#xff0c;一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功&am…