也说线程

也说线程

         作为java程序员,我们无时无刻都在和线程打交道。由于jvm为我们隐藏了太多的细节,我们很难透过层层迷雾去真正理解线程本身的性质。如果编写多线程的程序员不能充分理解线程的整体模型,编程过程中常常会出现一些莫名其妙的问题。掌握线程的设计初衷,也能为我们提供一个审视问题的角度,能够更加精准的使用线程。

         以下是我个人理解的一个线程图

          

         从上图我们可知,我们大部分时间是直接使用线程提供的相关API编程。所谓知其所以然,才能知其然。如果我们想灵活驾驭多线程,就应该去了解java线程模型(JMM)给我们所做的承诺,站在山顶,俯视山下的风景,那感觉才是最好的。我们重点去讨论同步过程中必须用到的两个基本概念,可见性和指令重排。

         我们都知道java的线程调度是交给操作系统的。这首先需要操作系统支持多线程。以linux为例,linux是以称之为轻量级进程来实现多线程的。我们不去关心轻量级进程具体如何实现,归根结底无非是一个复杂的数据结构(包括进程id,状态。。。。)和一套复杂的算法。有兴趣的读者可以用ps –eLlinux里查一下,如果里边有java进程,他会显示出所有的java线程映射到操作系统的轻量级进程信息。也就是我们的多线程程序经过jvm转化映射成linux能够‘认识‘的轻量级进程。对操作系统而言,他的目标就是不断的在众多的轻量级进程之间按照固定的规则(优先级,时间片等)来回切换。我们现在只需要简单理解“ java线程最后转成了操作系统可以理解的东西,运行后的一切操作都交给操作系统了”就可以了,我们将重点放在java线程模型上。

         在并发编程中,我们逃不过两个重要的问题:1,线程之间如何通信?2如何正确的同步多线程程序。对于第一个问题,一般会有两种方式,共享内存和消息通信。Java内存模型就是以共享内存的方式进程多个线程之间的数据通信的。不同于消息通信,共享内存是以隐式的方式进行线程通信的,这也是我们使用了这么久java都没有感觉到线程通信问题的原因,但是这种方式会使同步变得显式,平时被同步问题搞得焦头烂额,罪魁祸首都是共享内存惹得祸。对于消息通信,其实我们也不陌生,比如最经典的生产者消费者关系模型,javamq消息队列框架,都是源于这个思想。

         Java内存模型抽象

         我这里先给一个图,然后进行说明

 

 

由图可知,java内存模型包括了主内存与工作内存,JMM的主要目标就是定义程序中的共享变量从主内存读取到工作内存和从工作内存存储到主内存这样的底层细节。这里所说的共享变量是指:实例字段,静态字段和构成数组对象的元素,而不包括方法中的局部变量与方法参数,因为后者是线程私有的,不会产生数据竞争问题。注意:这里所说的主内存和工作内存与我们常说的堆,栈,方法区等并不是同一层次上的划分。如果非要把两者联系起来,大概的说,可以认为主内存对应的java堆中对象的实例数据部分,工作内存对应的java栈。

         下面我们说明一下主内存和工作内存之间的交互操作

4load

3read

线程B

 

变量副本



 
 

我们假设A线程改变主内存X的值,随后B线程读取该值。步骤如下1线程A通过store指令将本地变量传到主内存中,以便随后的write指令使用,2,通过write指令把刚才1的值存入主内存中,3线程B通过read指令将数据从主内存传输到线程B的工作内存中,以便随后的load使用,4load指令将3传入的值存在本地副本中,以供随后的操作数栈使用,至此,两个线程中间的交行操作已经完成了。注意:JMM只是要求store,writeread,load是成对的,并且顺序的执行,而没有要求必须是连续的,这一点很重要,比如我们举出一点可能,1,3,2,4,这将导致B线程读取了一个尚未改变的值,违背了我们的初衷,可以看到这就是多线程频频出现问题的罪魁祸首。下面我们来讨论一下为什么会设计成这个样子。

         要想说明以上问题,我们必须引出两个听说过的概念,可见性,与指令重新排序,对于上述问题实际上就是当A线程改变值的时候不能立即让线程B可见。无论是cpujmm,他们都有一个共同的目标,就是尽量提高其执行速度,以cpu为例,我们都知道cpu的执行速度与内存速度相差何止几个数量级。目前的机器cpu都自带缓存层,如下图 

 

 

由于cpu速度与内存速度相差太多,现在处理器常常采用读写缓存来提升速度,这里要说明的问题是cpu的本地缓存之间是不可见的,还有一个问题,cpu把数据先写到本地缓存,然后在之后合适的时候再写到内存中,并不保证写入的顺序是有序的,就像我们像车里装东西,在卸货的时候无法保证跟装上去的时候顺序完全一样,这就导致了指令重排,指令重排细节内容一会再说,单说可见性,如果想要达到一个线程的每条指令对另一个线程都是可见的,代价是相当大的,所以在没有做特殊标记的情况下,JMM并不要求所有指令的可见性。

         说完了可见性,我们系统的说一下指令重排,指令重排的意图是只要不影响(单线程)程序的执行结果,只要能够尽可能快的执行指令,而不去关注指令的具体执行顺序。Java代码从人类能看懂到计算机看懂要经过层层蜕变,如下图 
 

 

java程序到计算机可执行至少会经过三层过滤,

编译器在不改变单线程程序的运行结果的情况下可以对程序就行指令重排;

运行时执行引勤的动态调整,现在处理器也会采用指令并行计算来提高性能;

内存重排序在上一节已经说过了,cpu开启本地读写缓存,这也会导致指令重排。一句话,cpuJMM的目标是单位时间内执行更多的指令,弱化了指令的执行顺序,这与我们的逻辑思维是相违背的。我们来讨论一下重排序对多线程程序的影响。

 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来说,12是可以进行重新排序的,因为这丝毫不会影响最终执行的结果,现在的执行顺序成了2,3,4,1,导致b读取错误的值,线程B中的34也是可以互换顺序的,现在操作系统多用所谓分支预测来提升性能,如果预测成功,会导致4优先执行。这将出现不可预知的后果。这里还能说明一个问题,即便都是按顺序执行的,12执行完毕后并无法保证B线程能够立即看到结果,可能还在线程本地副本中。注意:本地副本是JMM模型的概念。并不一定真实存在,在实际过程中,可能是cpu寄存器,高速缓存等内容。

         至此,我们完全讨论完了java的线程模型,着重讨论的可见性与一致性(指令重排)的深层次原理。概况来说,JMM通过弱化可见性与一致性等手段来尽量提高程序的性能。我们现在知道了问题产生的原因,在处理同步问题的时候就相当有了准绳,从而引导我们更好的使用多线程技术.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值