您的位置:首页 > 编程语言 > Java开发

Java 多线程扩展之JMM

2016-05-15 17:13 429 查看
JMM(Java MemoryModel)​​

      内存模型:描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节。

      Java内存模型,其基本原则:​

       Happens-Before​:这个关系表示“一段
4000
代码在其他代码开始执行前已经执行完毕”。

       Synchronizes-with:这个关系表示“一个行为在开始发生时,它首先会将要操作的对象同主存同步完毕后才开始执行”​。

       他们之间的关系:

       Ifan action A synchronizes-with action B, then A happens-beforeB.​

      这两条规则都是说,releases happen beforeacquires(释放锁发生在获取锁之前)。​

       同步:在发出一个功能调用时,未得到响应之前,该调用不返回。如:一个线程A在执行的过程中,需要调用B提供一些数据,此时A向B发出请求,在B响应A的请求之前,A会停在这个位置上等待,不做其他事情,直到B响应。​

       异步:在发出一个功能调用时,不需要等待响应,继续做它自己的事情,一旦得到响应了再对响应做相应处理。​

       可见性:在多核或者多线程运行过程中内存的一种共享模式,在JMM模型里面,通过并发线程修改变量值的时候,必须将线程变量同步回主存过后,其他线程才可能访问到。​

       可排序性:提供了内存内部的访问顺序,在不同的程序针对不同的内存块进行访问的时候,其访问不是无序的。比如有一个内存块,A和B需要访问的时候,JMM会提供一定的内存分配策略有序地分配它们使用的内存,而在内存的调用过程也会变得有序地进行,内存的折中性质可以简单理解为有序性。而在Java多线程程序里面,JMM通过Java关键字volatile来保证内存的有序访问。​

        结构:JVM中存在一个主存区,Java的所有变量都存在主存,对于所有线程共享。每个线程都有自己的工作存储,工作存储中存的是主存的拷贝,线程对变量的所有操作都在自己的工作存储中,线程之间不能实现访问,变量在程序中的传递,是依赖主存实现的。​

可见性(Visibility)

      CPU的速度相对于内存来说是很高的,若CPU直接与内存去交互数据,速度的差异会使CPU经常处于空闲状态,为了解决这种浪费,在CPU中加入很多的寄存器、多级cache,比内存的速度快多了。

      某个线程执行时,它需要访问的内存中数据,会存于该线程的工作存储中(workingmemory,cache和寄存器的抽象,每个线程都有自己的工作存储),并于某个特定的时间写回内存​。单线程时,这不成问题。但多线程访问同一数据时,内存中的一个变量会存于多个​工作存储中,若某一线程修改了它工作存储中的变量,什么时候该对另外的线程可见该变量?编译器或运行时为了效率可以在允许的时候对指令进行重排序,重排序后指令的执行顺序就跟代码不一致了,此时,若另一线程获得CPU时间片来对其工作存储中的该变量进行修改,而先前的线程还没有将之前修改的变量写回到内存,这时就会有问题,这就是可见性的由来。

       ​共享变量实现可见性的原理:线程1修改变量,将工作存储1中修改过的共享变量刷新到主内存中,然后将主内存中最新的共享变量的值更新到线程2的工作存储中。这样就实现了共享变量在线程1和线程2之间的可见性。​

java语言层面有两种实现可见性的方法:Synchronized、volatile​

1. Synchronized实现可见性

线程解锁前,必须把工作存储中的共享变量的最新值刷新到主内存中;

线程加锁时,将清空工作存储中的共享变量的值,使用共享变量时必须重新从主内存中读取共享变量的最新值。​加锁和解锁必须是同一把锁。​

线程执行互斥代码的过程:​

(1)获得互斥锁​

(2)清空工作内存​

(3)从主内存拷贝变量的最新副本到工作存储中​

(4)执行代码​

(5)将更新后的共享变量刷新到主内存中​

(6)释放互斥锁

happens-before​偏序规则

       同一线程中,前面的所有写操作对后面的操作均可见;不同线程中,线程1所有的写操作都对要进行后续操作的线程2可见。如:

1. An unlock on a monitor happens-before every subsequent lockon thatmonitor.​​

如果线程1解锁了monitora,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。

2. A write to a volatile fieldhappens-before every subsequent read of that volatile.

如果线程1写入了volatile变量v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。

3. All actions in a thread happen-before any other threadsuccessfully returns from a join() on that thread.

线程t1写入的所有变量(所有action都与那个join有hb关系,当然也包括线程t1终止前的最后一个action了,最后一个action及之前的所有写入操作,所以是所有变量),在任意其它线程t2调用t1.join()成功返回后,都对t2可见。

4. Each action in a threadhappens-before every subsequent action in that thread.

线程中上一个动作及之前的所有写操作在该线程执行下一个动作时对该线程可见。​

       happens-before关系有个很重要的性质,就是传递性,即,如果hb(a,b),hb(b,c),则有hb(a,c)。如(代码中list对象是CopyOnWriteArrayList类型):​

                  线程1                                                                       线程2

                 p1:a= 1

                p2:list.set(1,"t")

                                                                                              p3:list.get(2)

                                                                                               p4:intb = a;​

若执行顺序为p1->p2->p3->p4,那最终的到的b是否是1?​​

分析如下:

p1,p2是同一线程,所以有 hb(p1,p2);​

p3,p4是同一线程,所以有 hb(p3,p4);​​

因为CopyOnWriteArrayList为并发集合,并发集合的读写也符合happens-before规则,因此有hb(p2,p3);​​

根据 happens-before 关系的传递性,得出结论
hb(p1,p4),所以 b的值为1。

当然,存在同步不正确的情况:

                        线程1                                                                     线程2

                      p1:a = 1

                                                                                                 p3:list.get(2)

                       p2:list.set(1,"t")

                                                                                                  p4:intb = a;​​

依然有hb(p1,p2),hb(p3,p4),但是没有了hb(p2,p3),得不到hb(p1,p4),jmm不保证最后b的值是1。如果程序没有采取手段(如加锁等)排除类似这样的执行顺序,那么无法保证b取到1。此时没有正确同步,存在着数据争用(datarace)。​

捎带同步:不为某一变量的读写设置同步,而是利用其它同步动作的捎带效果。如(result为普通变量):​

                          线程1                                                线程2

                     p1:result = v;

                     p2:releaseShared(0);

                                                             p3:tryAcquireSharedNanos(0,nanosTimeout)

                                                            p4:return result;​

若是以上执行顺序,根据happens-before的传递性,最后返回的result的值是v;但若出现以下的执行顺序,不就出现同步不正确了么?

 

                           线程1                                                   线程2

                   p1:result = v;

                                                            p3:tryAcquireSharedNanos(0, nanosTimeout)

                   p2:releaseShared(0);

                                                             p4:return result;

答案是不会,这里主要利用了AbstractQueuedSynchronizer中的releaseShared与tryAcquireSharedNanos存在的happens-before关系。分析如下:

若p2操作未执行,那么p3在执行tryAcquireSharedNanos时会一直被阻塞,直到releaseShared操作执行了或超过了nanosTimeout超时时间或被中断抛出InterruptedException;

若p2操作执行了,即releaseShared执行了,则就变成了第一种执行顺序,若是超时,那么返回值是false,代码逻辑中就直接抛出了异常,不会去取result了。

所以,这种执行顺序是不会出现的,也就不会有同步不正确的情况了,这样,虽然未对result做同步操作,但利用其他同步的捎带作用间接地实现了同步。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: