也说线程
作为java程序员,我们无时无刻都在和线程打交道。由于jvm为我们隐藏了太多的细节,我们很难透过层层迷雾去真正理解线程本身的性质。如果编写多线程的程序员不能充分理解线程的整体模型,编程过程中常常会出现一些莫名其妙的问题。掌握线程的设计初衷,也能为我们提供一个审视问题的角度,能够更加精准的使用线程。
以下是我个人理解的一个线程图
从上图我们可知,我们大部分时间是直接使用线程提供的相关API编程。所谓知其所以然,才能知其然。如果我们想灵活驾驭多线程,就应该去了解java线程模型(JMM)给我们所做的承诺,站在山顶,俯视山下的风景,那感觉才是最好的。我们重点去讨论同步过程中必须用到的两个基本概念,可见性和指令重排。
我们都知道java的线程调度是交给操作系统的。这首先需要操作系统支持多线程。以linux为例,linux是以称之为轻量级进程来实现多线程的。我们不去关心轻量级进程具体如何实现,归根结底无非是一个复杂的数据结构(包括进程id,状态。。。。)和一套复杂的算法。有兴趣的读者可以用ps –eL去linux里查一下,如果里边有java进程,他会显示出所有的java线程映射到操作系统的轻量级进程信息。也就是我们的多线程程序经过jvm转化映射成linux能够‘认识‘的轻量级进程。对操作系统而言,他的目标就是不断的在众多的轻量级进程之间按照固定的规则(优先级,时间片等)来回切换。我们现在只需要简单理解“ java线程最后转成了操作系统可以理解的东西,运行后的一切操作都交给操作系统了”就可以了,我们将重点放在java线程模型上。
在并发编程中,我们逃不过两个重要的问题:1,线程之间如何通信?2如何正确的同步多线程程序。对于第一个问题,一般会有两种方式,共享内存和消息通信。Java内存模型就是以共享内存的方式进程多个线程之间的数据通信的。不同于消息通信,共享内存是以隐式的方式进行线程通信的,这也是我们使用了这么久java都没有感觉到线程通信问题的原因,但是这种方式会使同步变得显式,平时被同步问题搞得焦头烂额,罪魁祸首都是共享内存惹得祸。对于消息通信,其实我们也不陌生,比如最经典的生产者消费者关系模型,java的mq消息队列框架,都是源于这个思想。
Java内存模型抽象
我这里先给一个图,然后进行说明
由图可知,java内存模型包括了主内存与工作内存,JMM的主要目标就是定义程序中的共享变量从主内存读取到工作内存和从工作内存存储到主内存这样的底层细节。这里所说的共享变量是指:实例字段,静态字段和构成数组对象的元素,而不包括方法中的局部变量与方法参数,因为后者是线程私有的,不会产生数据竞争问题。注意:这里所说的主内存和工作内存与我们常说的堆,栈,方法区等并不是同一层次上的划分。如果非要把两者联系起来,大概的说,可以认为主内存对应的java堆中对象的实例数据部分,工作内存对应的java栈。
下面我们说明一下主内存和工作内存之间的交互操作
4load |
3read |
线程B
|
变量副本 |
我们假设A线程改变主内存X的值,随后B线程读取该值。步骤如下1线程A通过store指令将本地变量传到主内存中,以便随后的write指令使用,2,通过write指令把刚才1的值存入主内存中,3线程B通过read指令将数据从主内存传输到线程B的工作内存中,以便随后的load使用,4,load指令将3传入的值存在本地副本中,以供随后的操作数栈使用,至此,两个线程中间的交行操作已经完成了。注意:JMM只是要求store,write与read,load是成对的,并且顺序的执行,而没有要求必须是连续的,这一点很重要,比如我们举出一点可能,1,3,2,4,这将导致B线程读取了一个尚未改变的值,违背了我们的初衷,可以看到这就是多线程频频出现问题的罪魁祸首。下面我们来讨论一下为什么会设计成这个样子。
要想说明以上问题,我们必须引出两个听说过的概念,可见性,与指令重新排序,对于上述问题实际上就是当A线程改变值的时候不能立即让线程B可见。无论是cpu还jmm,他们都有一个共同的目标,就是尽量提高其执行速度,以cpu为例,我们都知道cpu的执行速度与内存速度相差何止几个数量级。目前的机器cpu都自带缓存层,如下图
由于cpu速度与内存速度相差太多,现在处理器常常采用读写缓存来提升速度,这里要说明的问题是cpu的本地缓存之间是不可见的,还有一个问题,cpu把数据先写到本地缓存,然后在之后合适的时候再写到内存中,并不保证写入的顺序是有序的,就像我们像车里装东西,在卸货的时候无法保证跟装上去的时候顺序完全一样,这就导致了指令重排,指令重排细节内容一会再说,单说可见性,如果想要达到一个线程的每条指令对另一个线程都是可见的,代价是相当大的,所以在没有做特殊标记的情况下,JMM并不要求所有指令的可见性。
说完了可见性,我们系统的说一下指令重排,指令重排的意图是:只要不影响(单线程)程序的执行结果,只要能够尽可能快的执行指令,而不去关注指令的具体执行顺序。Java代码从人类能看懂到计算机看懂要经过层层蜕变,如下图
从java程序到计算机可执行至少会经过三层过滤,
编译器在不改变单线程程序的运行结果的情况下可以对程序就行指令重排;
运行时执行引勤的动态调整,现在处理器也会采用指令并行计算来提高性能;
内存重排序在上一节已经说过了,cpu开启本地读写缓存,这也会导致指令重排。一句话,cpu和JMM的目标是单位时间内执行更多的指令,弱化了指令的执行顺序,这与我们的逻辑思维是相违背的。我们来讨论一下重排序对多线程程序的影响。
Class Test{
Private int a;
Private boolean isTrue;
Public void set(){
a = 1;//1
isTrue = true;//2
}
Public void get(){
If(isTrue){//3
System.out.println(a);//4
}}
}
假设线程a先调用set方法,随后线程b调用get,打印a结果。以下画出了一种可能
对于线程A来说,1和2是可以进行重新排序的,因为这丝毫不会影响最终执行的结果,现在的执行顺序成了2,3,4,1,导致b读取错误的值,线程B中的3和4也是可以互换顺序的,现在操作系统多用所谓分支预测来提升性能,如果预测成功,会导致4优先执行。这将出现不可预知的后果。这里还能说明一个问题,即便都是按顺序执行的,1和2执行完毕后并无法保证B线程能够立即看到结果,可能还在线程本地副本中。注意:本地副本是JMM模型的概念。并不一定真实存在,在实际过程中,可能是cpu寄存器,高速缓存等内容。
至此,我们完全讨论完了java的线程模型,着重讨论的可见性与一致性(指令重排)的深层次原理。概况来说,JMM通过弱化可见性与一致性等手段来尽量提高程序的性能。我们现在知道了问题产生的原因,在处理同步问题的时候就相当有了准绳,从而引导我们更好的使用多线程技术.
相关推荐
此外,设置断点、查看线程调用堆栈以及分析线程同步状态也是常用的调试手段。 七、实际应用示例 在MFC应用中,例如,可以创建一个工作者线程用于长时间的数据库查询,而用户界面线程则保持正常响应,展示查询进度。...
总的来说,理解和掌握多线程之间的线程通信是编写高效、可靠并发程序的基础。通过合理设计和使用各种通信机制,我们可以创建出能够充分利用系统资源、处理复杂逻辑的多线程应用程序。在实际项目中,应始终关注线程...
### MFC多线程的创建详解 #### 一、MFC多线程概述 MFC (Microsoft Foundation Classes) 是微软为简化Windows程序开发提供的一套类库,它封装了Win32 API,使得开发者能够更加方便地进行Windows应用程序的开发。在...
多线程也有其挑战,比如线程间的数据共享可能导致竞态条件、死锁等问题,需要使用锁或其他同步机制来避免。此外,线程创建、销毁和管理也消耗资源,过多的线程可能会导致系统性能下降。 在实际编程中,如何选择单...
总结来说,"线程中创建子线程"是多线程编程的一个重要方面,它涉及线程的创建、管理和同步,以及如何利用这一机制提升程序的并发性和性能。在实际编程中,理解并掌握这些概念和技术,对于编写高效、可靠的多线程程序...
"系统线程(内核线程)和用户线程区别" 系统线程(内核线程)和用户线程是两种不同的线程模式,它们在实现和应用方面有很大的区别。 系统线程(内核线程)是由操作系统内核创建和撤销的线程,内核维护进程及线程的...
总的来说,VC++中的线程锁是多线程编程中保证数据一致性、防止竞态条件的重要手段。通过合理使用线程锁,我们可以编写出高效且安全的多线程程序。在提供的源码文件中,我们可以深入学习线程锁的实现细节,以及如何在...
《狂神说多线程详解》是一份深入探讨多线程技术的资源包,其中包含了对多线程编程的详尽解析。多线程是现代计算机编程中的一个重要概念,尤其在处理高性能计算、并发操作以及实时系统时,多线程技术显得尤为重要。它...
线程锁,也称为互斥锁,是一种同步机制,用于确保同一时间只有一个线程可以访问特定的资源或代码段。在LabWindows/CVI中,你可以使用内置的线程库函数来创建和管理线程锁。例如,`cvLock()` 和 `cvUnlock()` 函数...
本文将深入探讨如何挂起线程、休眠线程以及终止线程,这些都是多线程编程中的关键概念。 首先,让我们了解线程的基本概念。线程是程序执行的流程,每个进程至少有一个线程。在多线程环境中,多个线程可以共享同一...
5. **异常处理**:在编写线程代码时,应考虑异常处理,确保在出现异常时也能正确地关闭线程,避免程序崩溃。 6. **状态检查**:在线程运行过程中,定期检查是否需要退出,例如在循环体的开始或结束处检查一个全局或...
本文将深入探讨“C#内存释放-线程控制-线程启动-线程暂停”这一主题,结合提供的WFormsThread文件,我们可以假设这是一个关于Windows Forms应用程序中线程管理的实例。 首先,让我们关注线程控制。在C#中,我们通常...
总结来说,Delphi 7中的多线程测试揭示了并发编程的复杂性和挑战。通过理解线程管理、资源竞争以及合理的设计策略,可以优化多线程应用的性能。在实际项目中,开发者应根据具体情况调整线程数量,平衡并行度和系统...
在上述代码中,线程t被创建为detached状态,这意味着线程t在执行完毕后会自动释放资源,主线程不需要关心线程t的结束,也不会调用pthread_join()函数。 在实际编程中,我们需要根据线程执行的具体任务来决定线程的...
总结来说,C#中的多线程编程涉及到线程的创建、委托的使用以及线程间的通信和同步。通过合理地利用这些特性,开发人员可以构建出既高效又稳定的多线程应用程序。在处理多线程任务时,特别需要注意的是线程安全性和...
基于SpringBoot和POI实现单线程和多线程导出Excel.zip基于SpringBoot和POI实现单线程和多线程导出Excel.zip基于SpringBoot和POI实现单线程和多线程导出Excel.zip基于SpringBoot和POI实现单线程和多线程导出Excel.zip...
线程变量,也称为线程局部变量,是一种特殊类型的变量,它为每个线程维护独立的副本。这意味着,即使多个线程访问同一个线程局部变量,它们各自看到的都是自己独有的副本,不会相互影响。这种特性在多线程环境中非常...
易语言作为一款中国本土开发的编程语言,也提供了对多线程的支持。本篇将深入探讨易语言中如何实现多线程控制以及线程数量的管理。 在易语言中,线程用于在单个进程中同时执行多个独立的代码段,从而提高程序的执行...
在多线程编程中,线程间的协作是关键任务之一,尤其当需要一个线程在完成特定工作后通知另一个线程继续执行时。这个过程通常涉及到线程同步和异步的概念。本文将深入探讨线程异步工作以及如何在C++中实现一个线程在...
在C#中,由于使用线程和调用UI的线程属于两个不同的线程,如果在线程中直接设置UI元素的属性,此时就会出现跨线程错误。 下面介绍两种解决方案 第一种:使用控件自带的Invoke或者BeginInvoke方法。 Task....