Java 并发?内存模型综述!

版权声明:转载请随意! https://blog.csdn.net/qq_41723615/article/details/88959090

1.锁:

锁除了让临界区互斥外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的内存置为无效。从而使得被监视器保护的临界区代码必须从内存中读取共享变量。

锁释放与volatile写有相同的内存语义,锁获取与volatile读有相同的内存语义。

下面对锁释放和锁获取的内存语义做个总结。

·线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

·线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。·线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,马上我们会看到,这个volatile变量是ReentrantLock内存语义实现的关键。

ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁。

扫描二维码关注公众号,回复: 5774485 查看本文章

公平锁和非公平锁释放时,最后都要写一个volatile变量state。公平锁获取时,首先会去读volatile变量。非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

2.concurrent包的实现:

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式。

1)A线程写volatile变量,随后B线程读这个volatile变量。

2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量。

3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。

4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

concurrent包的实现示意图:

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基类来实现的。

3.final域的内存语义:

对于final域,编译器和处理器要遵守两个重排序规则。

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面。

1)JMM禁止编译器把final域的写重排序到构造函数之外。

2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已被正确初始化过了,而普通域不具有这个保障。

读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器的。

4.final域为引用类型

对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域
的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实,要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“逸出”。

5.happens-before

·对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。

·对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。

设计理念图:

只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

单线程程序是按程序的顺序来执行的。

正确同步的多线程程序是按happens-before指定的顺序来执行的。

6.双重检查锁:

需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。会采用延迟初始化。通过双重检查
锁定来降低同步的开销。多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。

重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

类初始化的处理过程:

第1阶段:通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁。

第2阶段:线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待。

第3阶段:线程A设置state=initialized,然后唤醒在condition中等待的所有线程。

第4阶段:线程B结束类的初始化处理。

第5阶段:线程C执行类的初始化的处理。

基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

本文只是对内存模型的基本了解,如果对并发编程出现的内存可见性问题有兴趣。请自行研究。

猜你喜欢

转载自blog.csdn.net/qq_41723615/article/details/88959090
今日推荐