Java并发编程(02 上)——Java 内存模型

1.引言

为了榨干计算机的运行能力,人们基于告诉缓存的存储交互解决了处理器与内存速度的矛盾,也引入了一个问题——缓存一致性。为了解决这个问题各个处理器访问缓存时都遵循一些协议,如:MSI,MESI,MOSI,Synapse等。这些协议都是对特定的内存或高速缓存进行读写访问的过程抽象,即“内存模型”。

2.并发编程模型的两个问题

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。线程之间的通信机制有两种:共享内存和消息传递。线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态
进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消
息来显式进行通信。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

3.Java 内存模型

Java内存模型(Java Memory Model)来屏蔽各种硬件和操作系统的内存访问差异。在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量(Local Variables),方法定义参数(Java语言规范称之为Formal Method Parameters)和异常处理器参数(ExceptionHandler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。JMM决定一个线程对共享变量的写入何时对另一个线程可见。

处理器,高速缓存,主内存间的交互关系 与 Java线程,主内存,工作内存交互关系 对比:
在这里插入图片描述
在这里插入图片描述
JMM中定义了8种操作(lock,unlock,read,load,use,assign,store,write)来完成一个变量从主内存拷贝到工作内存和从工作内存同步回主内存的操作。当然我们并不用直接操作这些,Java将这些操作进行了封装,如volatile。Java内存模型通过对这些操的包装为程序员提供了按需禁止缓存禁止指令重排的方法。

由JMM直接保证原子性变量的操作包括read,load,assign,use,store,write。我们可以认为基本数据类型的访问读写时具备原子性的。JMM还提供了lock和unlock操作来保证更大范围的原子性,其对应的字节码指令monitorenter和monitorexit,反应到Java代码中就是同步代码块——synchronized关键字。

JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种方式来实现可见性,无论时普通变量和volatile变量都时如此,唯一的区别是,volatile保证新值能立即同步到主内存,即每次使用前立即从主内存刷新,从而保证了多线程操作时变量的可见性,而普通变量做不到这一点,还有两个关键字可以保证其可见性,synchronized和final。

4.Happens-Before 规则

volatile变量只保证可见性,在Java1.5版本里使用happens-before规则对volatile语义进行了增强。它表达的意思时告诉编译器,对这个变量的读写,不能用CPU缓存,必须从主内存中读取或写入。
Happens-Before 规则可以理解为:前面一个操作的结果对后续操作是可见的。

4.1. 程序的顺序性规则

这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。

4.2. volatile 变量规则

对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。

4.3. 传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

4.4. 管程中锁的规则

这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

4.5. 线程 start() 规则

如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。

4.6. 线程 join() 规则

这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。

4.7. 线程中断规则

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

4.8. 对象终结规则

个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

5.final与this引用逃逸

final 修饰变量时,初衷是告诉编译器:这个变量生而不变。在下面例子中,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的。因此我们一定要避免“逸出”。

final int x;
// 错误的构造函数
public FinalFieldExample() { 
  x = 3;
  // 此处就是讲 this 逸出,
  global.obj = this;
}

this引用逃逸时很危险的事情,其他线程由可能通过这个引用访问到初始化一般的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值