技能开发 频道

一篇文章看懂Java并发和线程安全(一)

  【IT168 技能】前语

  长久以来,一向想剖析一下Java线程安全的实质,可是苦于有些微观的点想不理解,便放置了下来,前段时间渐渐想理解了,便把一切的点串联起来,趁着思路清晰,整理成这样一篇文章。

  导读

  为什么有多线程?

  线程安全描绘的实质问题是什么?

  Java内存模型(JMM)数据可见性问题、指令重排序、内存屏障

  揭晓答案

  为什么有多线程

  谈到多线程,咱们很容易与高性能画上等号,可是并非如此,举个简略的比如,从1加到100,用四个线程核算不一定比一个线程来得快。由于线程的创立和上下文切换,是一笔巨大的开支。

  那么规划多线程的初衷是什么呢?来看一个这样的实践比如,核算机一般需求与人来交互,假定核算机只要一个线程,而且这个线程在等候用户的输入,那么在等候的进程中,CPU什么工作也做不了,只能等候,形成CPU的利用率很低。假如规划成多线程,在CPU在等候资源的进程中,能够切到其他的线程上去,进步CPU利用率。

  现代处理器大多含有多个CPU中心,那么关于运算量大使命,能够用多线程的办法拆解成多个小使命并发的履行,进步核算的功率。

  总结起来无非两点,进步CPU的利用率、进步核算功率。

  线程安全的实质

  咱们先来看一个比如:

一篇文章看懂Java并发和线程安全(一)

  上面是一个把变量自增100次的比如,只不过用了4个线程,每个线程自增25次,用CountDownLatch等4个线程履行完,打印出终究成果。实践上,咱们期望程序的成果是100,可是打印出来的成果并非总是100。

  这就引出了线程安全所描绘的问题,咱们先用浅显的话来描绘一下线程安全:

  线程安全便是要让程序运转出咱们想要的成果,或许话句话说,让程序像咱们看到的那样履行。

  解释一下我总结的这句话,咱们先new出了一个add目标,调用了目标的doAdd办法,原本咱们期望每个线程有序的自增25次,终究得到正确的成果。假如程序增的像咱们预先设定的那样运转,那么这个目标便是线程安全的。

  下面咱们来看看Brian Goetz对线程安全的描绘:当多线程拜访一个目标时,假如不必考虑这些线程在运转时环境下的调度和替换,也不需求进行额定的同步,或许在调用方进行任何其他的和谐操作,调用这个目标的行为都能够取得正确的成果,那么这个目标便是线程安全的。

  下面咱们就来剖析这段代码为什么不能确保总是得到正确的成果。

  Java内存模型(JMM)数据可见性问题、指令重排序、内存屏障

  先从核算机的硬件功率说起,CPU的核算速度比内存快几个数量级,为了平衡CPU和内存之间的对立,引进的高速缓存,每个CPU都有高速缓存,乃至是多级缓存L1、L2和L3,那么缓存与内存的交互需求缓存一致性协议,这儿就不深化解说。那么终究处理器、高速缓存、主内存的交互联络如下:

一篇文章看懂Java并发和线程安全(一)

  那么Java的内存模型(Java Memory Model,简称JMM)也界说了线程、作业内存、主内存之间的联络,十分类似于硬件方面的界说。

一篇文章看懂Java并发和线程安全(一)

  这儿顺带提一下,Java虚拟机运转时内存的区域区分

  ·办法区:存储类信息、常量、静态变量等,各线程同享

  ·虚拟机栈:每个办法的履行都会创立栈帧,用于存储局部变量、操作数栈、动态链接等,虚拟机栈首要存储这些信息,线程私有

  ·本地办法栈:虚拟机运用到的Native办法服务,例如c程序等,线程私有

  ·程序计数器:记载程序运转到哪一行了,相当于当时线程字节码的行号计数器,线程私有

  ·堆:new出的实例目标都存储在这个区域,是GC的主战场,线程同享。

  所以关于JMM界说的主内存,大部分时分能够对应堆内存、办法区等线程同享的区域,这儿仅仅概念上对应,其实程序计数器、虚拟机栈等也有部分是放在主内存的,具体看虚拟机的规划。

  好了,了解了JMM内存模型,咱们来剖析一下,上面的程序为什么没得到正确的成果。请看下图,线程A、B一起去读取主内存的count初始值存放在各自的作业内存里,一起履行了自增操作,写回主内存,终究得到了过错的成果。

一篇文章看懂Java并发和线程安全(一)

      咱们再来深化剖析一下,形成这个过错的实质原因:

  ·可见性,作业内存的最新值不知道什么时分会写回主内存

  ·有序性,线程之间有必要是有序的拜访同享变量,咱们用“视界”这个概念来描绘一下这个进程,以B线程的视角看,当他看到A线程运算好之后,把值写回之内存之后,马上去读取最新的值来做运算。A线程也应该是看到B运算完之后,马上去读取,在做运算,这样就得到了正确的成果。

  接下来,咱们来具体剖析一下,为什么要从可见性和有序性两个方面来限制。

  给count加上volatile关键字,就确保了可见性。

  private volatile int count = 0;

  volatile关键字,会在终究编译出来的指令上加上lock前缀,lock前缀的指令做三件工作

  ·避免指令重排序(这儿对本问题的剖析不重要,后面会具体来讲)

  ·锁住总线或许运用确定缓存来确保履行的原子性,前期的处理可能用确定总线的办法,这样其他处理器没办法经过总线拜访内存,开支比较大,现在的处理器都是用确定缓存的办法,在合作缓存一致性来处理。

  ·把缓冲区的一切数据都写回主内存,并确保其他处理器缓存的该变量失效

  已然确保了可见性,加上了volatile关键词,为什么仍是无法得到正确的成果,原因是count++,并非原子操作,count++等效于如下过程:

  ·从主内存中读取count赋值给线程副本变量:temp=count

  ·线程副本变量加1:temp=temp+1

  ·线程副本变量写回主内存:count=temp

  就算是真的苛刻的给总线加锁,导致同一时间,只能有一个处理器拜访到count变量,可是在履行第(2)步操作时,其他cpu现已能够拜访count变量,此刻最新运算成果还没刷回主内存,形成了过错的成果,所以有必要确保次序性。

  那么确保次序性的实质,便是确保同一时间只要一个CPU能够履行临界区代码。这时分做法一般是加锁,锁实质是分两种:失望锁和达观锁。如典型的失望锁synchronized、JUC包下面典型的达观锁ReentrantLock。

  总结一下:要确保线程安全,有必要确保两点:同享变量的可见性、临界区代码拜访的次序性。

0
相关文章