解决多线程中11个常见问题
- 格式:docx
- 大小:50.75 KB
- 文档页数:12
JAVA多线程期末考试题库单选题100道及答案解析1. 在Java 中,实现多线程有几种方式?()A. 2 种B. 3 种C. 4 种D. 5 种答案:B解析:实现多线程有三种方式:继承Thread 类、实现Runnable 接口、使用线程池的Callable 和Future 。
2. 以下哪个方法用于启动一个线程?()A. start()B. run()C. begin()D. execute()答案:A解析:调用线程对象的start() 方法启动线程,会自动调用run() 方法执行线程任务。
3. 以下关于线程和进程的说法,错误的是()A. 一个进程可以包含多个线程B. 线程是进程的执行单元C. 进程之间不能共享内存D. 线程之间可以独立运行答案:D解析:线程之间共享进程的内存空间,不是独立运行的。
4. 以下哪个方法可以让当前线程暂停一段时间?()A. sleep()B. wait()C. notify()D. yield()答案:A解析:Thread.sleep() 方法可以让当前线程暂停指定的时间。
5. 以下哪个方法用于线程间的等待/通知机制?()A. sleep() 和notify()B. wait() 和notify()C. yield() 和notifyAll()D. suspend() 和resume()答案:B解析:wait() 使当前线程等待,notify() 唤醒一个等待的线程,notifyAll() 唤醒所有等待的线程。
6. 以下关于线程同步的说法,错误的是()A. 可以使用synchronized 关键字实现线程同步B. 同步会降低程序的并发性C. 同步可以保证线程安全D. 所有方法都应该同步答案:D解析:不是所有方法都需要同步,只有在多个线程可能同时访问和修改共享资源时才需要同步。
7. 以下哪个不是线程的状态?()A. 就绪B. 运行C. 阻塞D. 结束E. 暂停答案:E解析:线程的状态包括就绪、运行、阻塞和结束。
多线程同步的实现方法在多线程编程中,为了保证数据的正确性和程序的稳定性,需要使用同步机制来控制不同线程之间对共享资源的访问。
本文将介绍几种常见的多线程同步实现方法。
一、互斥锁互斥锁是最基本也是最常用的一种同步机制。
它通过对共享资源加锁来防止其他线程同时访问该资源,从而避免数据竞争和冲突问题。
当一个线程获得了该锁后,其他想要访问该资源的线程就必须等待其释放锁才能进行操作。
在C++11标准中提供了std::mutex类作为互斥量,在使用时可以调用lock()函数获取锁并执行相应操作,再调用unlock()函数释放锁。
需要注意的是,在使用时应尽可能缩小临界区范围以提高效率,并确保所有涉及到共享资源修改或读取操作都被包含在临界区内。
二、条件变量条件变量通常与互斥锁结合起来使用,用于协调不同线程之间对某个事件或状态变化进行响应和处理。
当某个条件满足时(如队列非空),唤醒等待该条件变量上阻塞着的一个或多个进入等待状态(wait)的进程,使其重新参与竞争获取所需资源。
C++11标准库中提供了std::condition_variable类作为条件变量,在使用前需要先创建一个std::unique_lock对象并传递给wait()函数以自动解除已有lock对象,并将当前进入等待状态直至被唤醒;notify_one() 和 notify_all() 函数则分别用于唤醒单个或全部处于等待状态下面向此条件变量发出请求者。
三、信号量信号量是一种更复杂但功能更强大的同步机制。
它通过计数器记录可用资源数量,并根据计数器值判断是否允许新建任务运行或者挂起正在运行任务以便其他任务可以获得所需资源。
其中P(Proberen)表示申请/获取信号灯, V(Verhogen)表示释放/归还信号灯.C++11标准库没有直接支持Semaphore,但我们可以利用mutex+condition_variable模拟实现Semaphore. 其核心思想就是:定义两个成员属性count_ 和 mutex_, count_ 表示当前可申请 Semaphore 的数量 , mutex_ 是 std::mutex 类型 , 定义两个成员方法 wait(), signal(). 四、原子操作原子操作指不能被打断、干扰或交错执行影响结果正确性的操作。
mfc多线程并发处理方式MFC多线程并发处理方式在MFC(Microsoft Foundation Class)框架中,多线程并发处理是一种常用的技术手段,用于实现并发执行多个任务,提高程序的性能和响应能力。
下面将介绍一些MFC中常用的多线程并发处理方式。
1. CWinThread类:CWinThread类是MFC中用于创建线程的基类。
可以通过派生CWinThread类并重写Run函数来实现自定义的线程逻辑。
多个CWinThread对象可以同时运行,实现任务的并发执行。
2. C++11标准线程库:MFC框架也支持使用C++11标准线程库来实现多线程并发处理。
通过包含<thread>头文件,可以使用std::thread类来创建、启动和加入线程,并通过lambda表达式或函数指针来指定线程的执行函数。
3. MFC消息映射机制:MFC中的消息映射机制可以实现GUI线程与工作线程之间的消息通信。
工作线程可以通过PostMessage或SendMessage函数向GUI 线程发送消息,GUI线程可以通过重写OnMessage函数来处理消息并更新用户界面。
4. 临界区和同步对象:在多线程访问共享资源时,为了避免数据竞争和结果的不确定性,可以使用MFC提供的临界区和同步对象。
临界区用于保护共享资源的访问,同步对象(如事件、互斥体、信号量)用于线程间的互斥和同步操作。
5. 并行模式:MFC框架也支持并行模式的开发,通过使用并行算法库(如parallel_invoke、parallel_for等),可以将任务自动分配给多个工作线程,并发地执行,从而提升程序的执行效率。
使用MFC的多线程并发处理方式可以充分利用多核处理器的能力,实现任务的并发执行,提高程序的性能和响应能力。
然而,需要注意在多线程编程中避免数据竞争和线程间的同步问题。
合理的线程调度、共享资源的保护和合适的同步机制都是确保多线程并发处理正确性和效率的关键。
多线程执行CPU过高问题推荐文章cpu鲁大师性评分排名热度:电脑cpu温度过高怎么办热度:iphone5s的详细参数热度: Linux中怎么限制CPU的占用率热度:Corei7CPU处理器性能区别热度:在项目开发过程中使用到多线程技术,有时程序运行起来占用CPU很高(具体占用多少,跟你的CPU核数有关。
CPU过高的问题,CPU多线程的问题,下面是店铺带来的关于多线程执行CPU过高问题的内容,欢迎阅读!多线程执行CPU过高问题:CPU占用高不高,跟你的线程数多少没有太多的影响。
因为若你CPU是双核,即使只创建了2个子线程,而这两个子线程是2个死循环,此时你的CPU占用也会是相当的高的。
不信的可以直接试试。
CPU占用高不高也不能只看本程序运行起来的CPU占用率,因为若是你的程序使用了内核对象的调用,那么在任务管理器中你会看到你的程序占用CPU是不高的,但使用到内核对象在内核调用中的那些服务程序就会显示CPU占用高。
[cpp] view plain?1.int _tmain(int argc, _TCHAR* argv[])2.{3.InitializeCriticalSection(&m_gCriticalSection);4.//一个线程最多可以挂起 MAXIMUM_SUSPEND_COUNT次,即127次5.//线程16.unsigned threadID;7.hThread1 = (HANDLE)_beginthreadex(NULL, 0, ThreadPr oc1, NULL, 0, &threadID);9.//线程210.unsigned threadID2;11.hThread2 = (HANDLE)_beginthreadex(NULL, 0, Threa dProc2, NULL, 0, &threadID2);[cpp] view plain?1.return 0;[cpp] view plain?1.unsigned __stdcall ThreadProc1(void* pParam)2.{3.//打印数字iCount4.if(0 == m_gCount)5.{6.Sleep(2000);7.}8.9.while(1)10.{11.EnterCriticalSection(&m_gCriticalSection);12.m_gCount++;13.<span style="color:#ff0000;">cout << "线程一:" << m_gCount << endl; //两个线程函数中的的这句不注释掉,该程序在任务管理中显示占用的CPU是不高的,但内核调用占用CPU 很高</span>[cpp] view plain?1.<span style="color:#ff0000;"> //若是注释掉,则程序在任务管理器中显示占用的CPU就会很高2.</span> LeaveCriticalSection(&m_gCriticalSection);4.//Sleep(1000);5.}6.7.return 0;8.}9.10.unsigned __stdcall ThreadProc2(void* pParam)11.{12.//打印数字iCount13.while(1)14.{15.EnterCriticalSection(&m_gCriticalSection);16.m_gCount++;17.<span style="color:#ff0000;">cout << "另一线程:" << m_gCount << endl;18.</span> LeaveCriticalSection(&m_gCriticalSection );19.20.//Sleep(1000);21.}22.23.return 0;24.}。
java.util.ConcurrentModificationException异常问题详解环境:JDK 1.8.0_111在Java开发过程中,使⽤iterator遍历集合的同时对集合进⾏修改就会出现java.util.ConcurrentModificationException异常,本⽂就以ArrayList为例去理解和解决这种异常。
⼀、单线程情况下问题分析及解决⽅案1.1 问题复现先上⼀段抛异常的代码。
1public void test1() {2 ArrayList<Integer> arrayList = new ArrayList<>();3for (int i = 0; i < 20; i++) {4 arrayList.add(Integer.valueOf(i));5 }67// 复现⽅法⼀8 Iterator<Integer> iterator = arrayList.iterator();9while (iterator.hasNext()) {10 Integer integer = iterator.next();11if (integer.intValue() == 5) {12 arrayList.remove(integer);13 }14 }1516// 复现⽅法⼆17 iterator = arrayList.iterator();18for (Integer value : arrayList) {19 Integer integer = iterator.next();20if (integer.intValue() == 5) {21 arrayList.remove(integer);22 }23 }24 }在这个代码中展⽰了两种能抛异常的实现⽅式。
1.2、问题原因分析先来看实现⽅法⼀,⽅法⼀中使⽤Iterator遍历ArrayList,抛出异常的是iterator.next()。
C++11多线程–Part8:std::future,std::promise以及线程的返回值std::future对象可以与async,std::packaged_task和std::promise⼀起使⽤。
本⽂主要关注将std::future与std::promise对象⼀起使⽤。
很多时候,我们遇到希望线程返回结果的情况。
现在的问题是如何做到这⼀点?让我们举个例⼦假设在我们的应⽤程序中,我们创建了⼀个压缩给定⽂件夹的线程,并且我们希望该线程返回新的zip⽂件名及其⼤⼩。
现在,我们有两种⽅法1.) 旧⽅法:使⽤指针在线程之间共享数据将指针传递给新线程,该线程将在其中设置数据。
然后直到在主线程中使⽤条件变量继续等待。
当新线程设置数据并向条件变量发出信号时,主线程将唤醒并从该指针获取数据。
为了简单起见,我们使⽤了⼀个条件变量,⼀个互斥量和⼀个指针,即3个项来捕获返回的值。
现在假设我们希望该线程在不同的时间点返回3个不同的值,那么问题将变得更加复杂。
是否有⼀个简单的解决⽅案可以从线程返回值。
使⽤std::future的答案是肯定的,为此查看下⼀个解决⽅案。
2.) C++11⽅式:使⽤std::future和std::promisestd::future是⼀个类模板,其对象存储将来的值。
现在这个未来的值到底是什么。
实际上,⼀个std::future对象在内部存储了将分配的值,并且还提供了⼀种访问该值的机制,即使⽤get()成员函数。
但是,如果有⼈尝试在get()函数可⽤之前访问future的此关联值,则get()函数将阻塞直到该值不可⽤。
std::promise也是⼀个类模板,其对象承诺将来会设置该值。
每个std::promise对象都有⼀个关联的std::future对象,⼀旦该值由std::promise对象设置,它将给出该值。
⼀个std::promise对象与其关联的std::future对象共享数据。
技术难点及解决方案引言技术领域的发展与进步离不开不断突破的技术难点。
在软件开发过程中,不同项目和不同领域都有其独特的技术难题。
本文将探讨一些常见的技术难点,并提供相应的解决方案。
一、性能优化问题描述在大型软件项目中,性能优化是一个常见的挑战。
当软件运行过程中遇到性能瓶颈,会导致应用变得缓慢,响应时间延长或者崩溃。
解决方案1.通过代码剖析工具定位性能瓶颈,如使用火焰图等分析工具可以帮助理解代码中的性能瓶颈。
2.使用缓存技术,在热点数据上使用缓存,可以减少数据库或其他外部资源的访问时间,提高响应速度。
3.在代码层面上进行优化,如避免无谓的循环,减少内存占用等。
同时,使用高效的算法和数据结构,可以提高性能。
4.异步调用和并行处理是提高性能的有效手段。
合理利用多线程、进程或者分布式计算等技术,可以充分利用系统资源,加快运算速度。
二、安全性问题问题描述随着网络应用的普及,系统安全性成为关注的焦点。
黑客攻击、数据泄露、权限问题等都是常见的安全性问题。
解决方案1.应用多层次的安全模型,包括身份验证、权限控制、访问控制等,以确保用户只能访问他们所需的数据和操作。
2.对敏感数据进行加密存储,在数据传输过程中使用 HTTPS 或其他安全协议来保护数据的传输安全。
3.定期进行安全审计,发现安全风险并及时修复。
4.对代码进行安全审查,避免常见的安全漏洞,如跨站脚本攻击(XSS)和 SQL 注入等。
5.给予用户清晰的安全提示和建议,提高用户安全意识。
三、跨平台兼容性问题描述随着移动互联网的快速发展,软件需要在不同的操作系统、不同的终端上运行,跨平台兼容性成为一个重要的挑战。
解决方案1.使用跨平台的开发框架,如React Native、Flutter等,可以大幅减少开发跨平台应用的复杂性。
2.采用响应式设计原则,使应用根据用户设备的屏幕尺寸和分辨率灵活适配,保证用户体验一致性。
3.严格按照各平台的规范和标准进行开发,避免使用特定平台的特性,减少兼容性问题。
软件开发中最常见的24种错误类型及其解决方案在软件开发中,无论是新手还是经验丰富的开发人员,都难免会遇到各种各样的错误和挑战。
这些错误可能来自不同的层面,比如编码、测试、部署和维护等。
为了帮助开发人员更好地解决这些问题,本文总结了软件开发中最常见的24种错误类型,并提供了相应的解决方案。
1. 死锁错误死锁是一种多线程执行过程中常见的错误类型。
当多个线程都在等待某个资源的时候,就有可能出现死锁。
这种错误通常会导致程序停止响应,无法正常执行。
解决方案:通过合理规划线程代码顺序,减少出现死锁的概率。
对于已经出现死锁的情况,可以通过进程管理工具来手动结束进程。
2. 内存泄漏错误内存泄漏是指程序在运行时分配的内存空间没有被释放,导致程序在长时间运行后出现崩溃或者异常。
这种错误通常会难以定位,因为它不会立即导致程序崩溃。
解决方案:通过代码审查和内存泄漏检测工具找出问题代码,并在代码中添加适当的释放内存的语句。
3. 缓存不一致错误在分布式系统中,缓存是一种常见的技术,用于提高系统性能。
然而,由于缓存的更新机制存在一定的滞后性,当多个系统同时访问某个缓存时,就容易出现缓存不一致的情况,导致数据不准确或者出现异常。
解决方案:利用分布式缓存系统或者锁机制,实现缓存的同步更新,避免不一致的情况。
4. 空指针错误空指针错误是指程序中使用了空指针变量,导致程序崩溃或者出现异常。
这种错误通常由于变量没有被初始化或者被误删除导致。
解决方案:在程序中对变量进行合适的初始化,并添加空指针判断,确保变量不是空指针。
5. 栈溢出错误栈溢出是指程序在执行中使用了过多的栈空间,导致程序崩溃或者异常。
这种错误通常由于递归调用、过深的函数调用链等因素引起。
解决方案:对程序进行优化和重构,减少递归调用的次数和深度,并分离长函数实现。
6. 逻辑错误逻辑错误是指程序在实现业务逻辑时出现的错误,导致程序无法正确执行。
这种错误通常由于实现逻辑不完整或者存在逻辑漏洞引起。
Java并发编程中的常见问题与解决方案Java并发编程已经成为了Java开发者必备的技能之一。
然而,在实际的开发中,往往会遇到各种各样的问题,这些问题不仅会影响到程序的性能和可靠性,还可能引发严重的安全问题。
本文将介绍Java并发编程中的常见问题和解决方案,帮助开发者提高代码质量和效率。
一、线程安全问题在多线程程序中,线程安全问题是最普遍的问题。
当多个线程同时访问同一个共享资源时,就有可能发生竞态条件,也就是多个线程对同一个资源进行非原子操作,导致程序出现错误。
例如,在下面这段代码中:```javapublic class Counter {private int count;public synchronized void increment() {count ++;}public synchronized int getCount() {return count;}}public class Main {public static void main(String[] args) {Counter counter = new Counter();for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {counter.increment();}}).start();}System.out.println("Count: " + counter.getCount());}}```这段代码用了一个计数器来统计所有线程对其进行increment()操作后的结果。
这里使用了synchronized关键字来保证increment()和getCount()方法的原子性。
但是,当计数器被多个线程访问时,即使使用了同步关键字,也可能会出现线程安全问题。
为了解决这个问题,可以使用Java的并发包中的AtomicInteger 类,它提供了一系列操作方法,保证了多线程间对整数变量的原子操作。
并发危险解决多线程代码中的11 个常见的问题Joe Duffy本文将介绍以下内容:▪基本并发概念▪并发问题和抑制措施▪实现安全性的模式▪横切概念本文使用了以下技术:多线程、.NET Framework目录数据争用忘记同步粒度错误读写撕裂无锁定重新排序重新进入死锁锁保护戳记两步舞曲优先级反转实现安全性的模式不变性纯度隔离并发现象无处不在。
服务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器的日益普及,客户端程序也将需要执行一些任务。
随着并发操作的不断增加,有关确保安全的问题也浮现出来。
也就是说,在面对大量逻辑并发操作和不断变化的物理硬件并行性程度时,程序必须继续保持同样级别的稳定性和可靠性。
与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。
对内存的读写以及对共享资源的访问必须使用同步机制进行管制,以防发生冲突。
另外,通常有必要对线程进行协调以协同完成某项工作。
这些附加要求所产生的直接结果是,可以从根本上确保线程始终保持一致并且保证其顺利向前推进。
同步和协调对时间的依赖性很强,这就导致了它们具有不确定性,难于进行预测和测试。
这些属性之所以让人觉得有些困难,只是因为人们的思路还未转变过来。
没有可供学习的专门API,也没有可进行复制和粘贴的代码段。
实际上的确有一组基础概念需要您学习和适应。
很可能随着时间的推移某些语言和库会隐藏一些概念,但如果您现在就开始执行并发操作,则不会遇到这种情况。
本文将介绍需要注意的一些较为常见的挑战,并针对您在软件中如何运用它们给出一些建议。
首先我将讨论在并发程序中经常会出错的一类问题。
我把它们称为“安全隐患”,因为它们很容易发现并且后果通常比较严重。
这些危险会导致您的程序因崩溃或内存问题而中断。
当从多个线程并发访问数据时会发生数据争用(或竞争条件)。
特别是,在一个或多个线程写入一段数据的同时,如果有一个或多个线程也在读取这段数据,则会发生这种情况。
之所以会出现这种问题,是因为Windows 程序(如C++ 和Microsoft .NET Framework 之类的程序)基本上都基于共享内存概念,进程中的所有线程均可访问驻留在同一虚拟地址空间中的数据。
静态变量和堆分配可用于共享。
请考虑下面这个典型的例子:static class Counter {internal static int s_curr = 0;internal static int GetNext() {return s_curr++;}}Counter 的目标可能是想为GetNext 的每个调用分发一个新的唯一数字。
但是,如果程序中的两个线程同时调用GetNext,则这两个线程可能被赋予相同的数字。
原因是s_curr++ 编译包括三个独立的步骤:1.将当前值从共享的s_curr 变量读入处理器寄存器。
2.递增该寄存器。
3.将寄存器值重新写入共享s_curr 变量。
按照这种顺序执行的两个线程可能会在本地从s_curr 读取了相同的值(比如42)并将其递增到某个值(比如43),然后发布相同的结果值。
这样一来,GetNext 将为这两个线程返回相同的数字,导致算法中断。
虽然简单语句s_curr++ 看似不可分割,但实际却并非如此。
忘记同步这是最简单的一种数据争用情况:同步被完全遗忘。
这种争用很少有良性的情况,也就是说虽然它们是正确的,但大部分都是因为这种正确性的根基存在问题。
这种问题通常不是很明显。
例如,某个对象可能是某个大型复杂对象图表的一部分,而该图表恰好可使用静态变量访问,或在创建新线程或将工作排入线程池时通过将某个对象作为闭包的一部分进行传递可变为共享图表。
当对象(图表)从私有变为共享时,一定要多加注意。
这称为发布,在后面的隔离上下文中会对此加以讨论。
反之称为私有化,即对象(图表)再次从共享变为私有。
对这种问题的解决方案是添加正确的同步。
在计数器示例中,我可以使用简单的联锁:static class Counter {internal static volatile int s_curr = 0;internal static int GetNext() {return Interlocked.Increment(ref s_curr);}}它之所以起作用,是因为更新被限定在单一内存位置,还因为(这一点非常方便)存在硬件指令(LOCK INC),它相当于我尝试进行原子化操作的软件语句。
或者,我可以使用成熟的锁定:static class Counter {internal static int s_curr = 0;private static object s_currLock = new object();internal static int GetNext() {lock (s_currLock) {return s_curr++;}}}lock 语句可确保试图访问GetNext 的所有线程彼此之间互斥,并且它使用CLRSystem.Threading.Monitor 类。
C++ 程序使用CRITICAL_SECTION 来实现相同目的。
虽然对这个特定的示例不必使用锁定,但当涉及多个操作时,几乎不可能将其并入单个互锁操作中。
粒度错误即使使用正确的同步对共享状态进行访问,所产生的行为仍然可能是错误的。
粒度必须足够大,才能将必须视为原子的操作封装在此区域中。
这将导致在正确性与缩小区域之间产生冲突,因为缩小区域会减少其他线程等待同步进入的时间。
例如,让我们看一看图1所示的银行帐户抽象。
一切都很正常,对象的两个方法(Deposit 和Withdraw)看起来不会发生并发错误。
一些银行业应用程序可能会使用它们,而且不担心余额会因为并发访问而遭到损坏。
图1 银行帐户class BankAccount {private decimal m_balance = 0.0M;private object m_balanceLock = new object();internal void Deposit(decimal delta) {lock (m_balanceLock) { m_balance += delta; }}internal void Withdraw(decimal delta) {lock (m_balanceLock) {if (m_balance < delta)throw new Exception("Insufficient funds");m_balance -= delta;}}}但是,如果您想添加一个Transfer 方法该怎么办?一种天真的(也是不正确的)想法会认为由于Deposit 和Withdraw 是安全隔离的,因此很容易就可以合并它们:class BankAccount {internal static void Transfer(BankAccount a, BankAccount b, decimal delta) {Withdraw(a, delta);Deposit(b, delta);}// As before}这是不正确的。
实际上,在执行Withdraw 与Deposit 调用之间的一段时间内资金会完全丢失。
正确的做法是必须提前对 a 和 b 进行锁定,然后再执行方法调用:class BankAccount {internal static void Transfer(BankAccount a, BankAccount b, decimal delta) {lock (a.m_balanceLock) {lock (b.m_balanceLock) {Withdraw(a, delta);Deposit(b, delta);}}}// As before}事实证明,此方法可解决粒度问题,但却容易发生死锁。
稍后,您会了解到如何修复它。
读写撕裂如前所述,良性争用允许您在没有同步的情况下访问变量。
对于那些对齐的、自然分割大小的字—例如,用指针分割大小的内容在32 位处理器中是32 位的(4 字节),而在64 位处理器中则是64 位的(8 字节)—读写操作是原子的。
如果某个线程只读取其他线程将要写入的单个变量,而没有涉及任何复杂的不变体,则在某些情况下您完全可以根据这一保证来略过同步。
但要注意。
如果试图在未对齐的内存位置或未采用自然分割大小的位置这样做,可能会遇到读写撕裂现象。
之所以发生撕裂现象,是因为此类位置的读或写实际上涉及多个物理内存操作。
它们之间可能会发生并行更新,并进而导致其结果可能是之前的值和之后的值通过某种形式的组合。
例如,假设ThreadA 处于循环中,现在需要仅将0x0L 和0xaaaabbbbccccddddL 写入64 位变量s_x 中。
ThreadB 在循环中读取它(参见图2)。
图2 将要发生的撕裂现象internal static volatile long s_x;void ThreadA() {int i = 0;while (true) {s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;i++;}}void ThreadB() {while (true) {long x = s_x;Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);}}您可能会惊讶地发现ThreadB 的声明可能会被触发。
原因是ThreadA 的写入操作包含两部分(高32 位和低32 位),具体顺序取决于编译器。
ThreadB 的读取也是如此。
因此ThreadB 可以见证值0xaaaabbbb00000000L 或0x00000000aaaabbbbL。
无锁定重新排序有时编写无锁定代码来实现更好的可伸缩性和可靠性是一种非常诱人的想法。
这样做需要深入了解目标平台的内存模型(有关详细信息,请参阅Vance Morrison 的文章"Memory Models:Understand the Impact of Low-Lock Techniques in Multithreaded Apps",网址为/magazine/cc163715)。
如果不了解或不注意这些规则可能会导致内存重新排序错误。
之所以发生这些错误,是因为编译器和处理器在处理或优化期间可自由重新排序内存操作。
例如,假设s_x 和s_y 均被初始化为值0,如下所示:internal static volatile int s_x = 0;internal static volatile int s_xa = 0;internal static volatile int s_y = 0;internal static volatile int s_ya = 0;void ThreadA() {s_x = 1;s_ya = s_y;}void ThreadB() {s_y = 1;s_xa = s_x;}是否有可能在ThreadA 和ThreadB 均运行完成后,s_ya 和s_xa 都包含值0?看上去这个问题很可笑。