《分布式JAVA应用 基础与实践》 第三章 3.3 JVM线程资源同步及交互机制(一)...

3.3  JVM线程资源同步及交互机制

Java程序采用多线程的方式来支撑大量的并发请求处理,程序在多线程方式执行的情况下,复杂程度远高于单线程串行执行的程序。尤其是在多核或多 CPU系统中,多线程执行的程序所带来的最明显的问题是线程之间共同管理的资源的竞争及线程之间的交互。JVM的线程实现及调度方式(抢占式、协作式)取 决于操作系统,超出了本书范围,本节中仅介绍JVM线程资源同步机制和线程之间的交互机制。

3.3.1  线程资源同步机制

首先来看典型获取下一ID的程序:

 
  1. int i = 0 ; public int getNextId(){ return i++; }

以下为上面程序在JVM中的执行步骤:

1) JVM首先在main memory(JVM堆) 给i分配一个内存存储场所,并存储其值0;

2)线程启动后,会自动分配一片working memory区(通常是操作数栈),当线程执行到return i++时,JVM中并不是简单的一个步骤就可以完成的。i++动作在JVM中分为装载i、读取i、进行i+1操作、存储i及写入i五个步骤才得以完成。

装载i

线程发起一个装载i的请求给jvm线程执行引擎,引擎接收请求后会向main memory发起一个read i的指令。

当read i执行完毕后,一段时间线程会将i的值从main memory区复制到working memory区中。

读取i

此步负责的是从main memory中读取i。

进行i+1操作

此步由线程完成。

存储i

将i+1的值赋给i,然后存储到working memory中。

写入i

一段时间后i的值会写回到main memory中。

从以上步骤描述来看,这里面最关键的问题有两点:一是working memory中i值和main memory中的i值的同步是需要时间的;二是i++由多个操作组成。只要多个线程在这个时间段内同时执行了操作,就会出现获取i值相同的现象。举个简单 的例子:假设线程A已执行i+1操作,但尚未执行完写入i步骤,线程B就完成了装载i的过程,那么当线程B执行完毕时,其得到的值和A就是一样的了,这是 以上代码在多线程执行时会出现获取相同值的原因。

JVM把对于working memory的操作分为了use、assign、load、store、lock和unlock,对于working memory的操作的指令由线程发出,对于main memory的操作分为了read、write、lock和unlock;对于main memory的操作的指令由线程执行引擎发出 ,其含义分别为:

use

use由线程发起,需要完成将变量的值从working memory中复制到线程执行引擎中。

assign

assign由线程发起,需要完成将变量值复制到线程的working memory中,例如a=i,这时线程就会发起一个assign动作。

load

load由线程发起,需要完成将main memory中read到的值复制到working memory中。

store

store由线程发起,负责将变量的值从working memory中复制到main memory中,并等待main memory通过write动作写入此值。

 

read

read由main memory发起,负责从main memory中读取变量的值。

write

write由main memory发起,负责将working memory的值写入到main memory中。

lock

lock动作由线程发起,同步操作main memory,给对象加上锁。

unlock

unlock动作由线程发起,同步操作main memory,去除对象的锁。

JVM中保证以下操作是顺序的:

1)同一个线程上的操作一定是顺序执行的;

2)对于main memory上的同一个变量的操作一定是顺序执行的,也就是不可能两个请求同时读取变量值;

3)对于加了锁的main memory上的对象操作,一定是顺序执行的,也就是两个以上加了lock的操作,同时肯定只有一个是在执行的。

为了避免资源操作的脏数据问题,JVM提供了synchronized关键字、volatile关键字和lock/unlock机制。

采用synchronized关键字改造上面的代码为:

 
  1. public synchronized int getNextId(){ return i++; }

当多线程执行此段代码时,线程A执行到getNextId()方法,JVM知道该方法上有synchronized关键字,于是在执行其他动作前首 先按照对象的实例ID加上一个lock。然后再继续执行return i++,而此时如线程B并发访问getNextId()方法,JVM观察到这个对象的实例ID上有lock,于是将线程B放入等待执行的队列中,只有当线 程A的return i++执行完毕后,JVM才会释放对象实例ID上的lock,重新标记为unlock。这时当下次线程调度到线程B时,线程B才得以执行 getNextId()方法,由于这个过程是串行的,因此可以保证每个线程getNextId都是不一样的值。

synchronized除了可直接写在方法上外,也可直接定义在对象上,区别仅在于JVM会根据这些情况来决定lock标记是打在什么上。需要注意的是,如果synchronized是标记在一个static方法上,那么执行时锁的粒度为当前Class。

lock/unlock机制的原理和synchronized相同,synchronized、lock/unlock机制都可用于保证某段代码执 行的原子性,由于锁会阻塞其他线程同样需要锁的部分的执行,因此在使用锁机制时要避免死锁的产生,尤其是在多把锁的情况下,例如:

 
  1. private Object a = new Object(); private Object b = new Object(); public void callAB(){ synchronized(b){ synchronized(a){ // Do something; } } } public void executeAB(){ synchronized(a){ synchronized(b){ // Do something; } } }

当上面的callAB和executeAB被两个线程同时执行时,就会产生死锁的现象,导致系统挂起。这是一个使用synchronized的例 子,对于直接使用lock/unlock来编写的多线程程序而言,一定要保证lock和unlock是成对出现的,并且要保证lock后程序执行完毕时一 定要unlock,否则就有线程会出现锁饿死的现象。

volatile的机制有所不同,它仅用于控制线程中对象的可见性,但并不能保证在此对象上操作的原子性。就像上面i++的场景,即使把i定义为 volatile也是没用的,对于定义为volatile的变量,线程不会将其从main memory复制到work memory中,而是直接在main memory中进行操作。它的代价虽然比synchronized、lock/unlock低,但用起来要非常小心,毕竟它不能保证操作的原子性。

有了synchronized、lock/unlock及volatile后,在Java中就可以很好地控制多线程程序中资源的竞争。但由于这三个机制对于系统的性能都是有影响的,例如会将操作由并行化变为串行化,因此需要根据合理的需求对线程中的资源进行同步。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值