有几种方式能实现多个c 线程共享变量量之间的happens-before方式

人法地,地法天,天法道,道法自然
记得很久前我去参与一次Java面试,面试官问了一道单例模式设计相关的问题。问题大概是这样,先考察一下我对单例模式的了解情况,毕竟这是GoF经典书籍《设计模式》中的一个经典的模式。在确认了我基本清楚这个设计模式后,让我用Java代码写一个单例模式的类,于是我给出了如下代码段。
class SingletonEx
private static SingletonEx instance =
private SingletonEx(){}
public static SingletonEx getInstance()
if( instance == null )
instance = new SingletonEx();
如上,把构造方法和instance对象设置为private则避免外部构造多个实例,并对单例的对象instance做了延迟初始化,第一次使用的时候做初始化。但这是不考虑并发的情况,像上篇文章所说的,instance的为空检测和实例化赋值不能保持原子性,于是又了下面的版本。
public synchronized static SingletonEx getInstance()
if( instance == null )
instance = new SingletonEx();
好了,这个并发运行逻辑上是没什么大问题了,我基本上也就给了这个答案。但这看起来会有效率问题,即使instance已经被创建过了,所有的线程还是要等这个方法的锁,卡在这个方法上。于是我们通常会考虑到缩小锁的范围,让更多的线程顺利通过,但只把synchronized块放在方法里面并没有缩小范围,还要再在外面加一个判断,好让线程走“绿色通道”,于是就有下面版本:
public static SingletonEx getInstance()
if( instance == null )
synchronized(SingletonEx.class)
if( instance == null )
instance = new SingletonEx();
嗯,这样虽然看起来比较啰嗦,但至少在功能上貌似是完满了!但是……这实际上会有隐患……
上面我从一个看起来不起眼的单例实现问题开始,讲到了并发的情况,关于最后的问题在哪里?如何解决这个问题,正是下面要说的Java内存模型(JMM)和重排序。
相信计算机相关专业的朋友们在学校都学过“计算机组成原理”(也有的叫“计算机组成结构”“计算机体系结构”),我们都知道由于物理实现原因,从处理器到缓存存取、再到主存存取、再到辅存存取(比如硬盘),速度是依次降低的。为了保证处理器计算资源的高效利用,处理器内部设计了寄存器和高速缓存,这样不必在大部分情况下等待存取完成而浪费时间。在多处理器结构中也会有更复杂的考虑。
我们同时也知道在Java体系结构的设计中,JVM在很大程度上屏蔽了底层的实现,使得开发人员不必过分关注底层的细节。Java内存模型也是一个相对于底层设计来讲比较抽象的模型结构。通常来说,为了保证特定线程不被其它其它因素所影响高速执行,每个线程都有对应的“工作内存”,而与此对应的,各线程最终都可以看到的是“主内存区”,这和缓存有点类似。在定义了这样两个内存区基础上,JMM还定义了在这两个内存区的一些基本操作(如在工作内存和主内存间的存取数据转移)和对这些操作的一些要求(存取操作的顺序要求和成对出现等)。
多处理器情况下内存模型示意图
(上图来自于)
根据对JMM的系统描述,通常JMM会被归纳为有三个特性:原子性、可见性、有序性。根据周志明的《深入理解Java虚拟机》,JMM中的操作有如下8种,分别是read、load、use、assign、store、write以及lock和unlock。其中前6种(不考虑long和double)可以被认为是满足原子性的,更大范围的原子性要lock和unlock来保证,对应于锁实现。对于可见性,举个例子就是工作内存里面被线程改了,但未会写到主内存,其它线程就不可见。有序性有这样一句话“本线程内观察,线程内的所有操作有序,一个线程观察另外一个则无序”。说到这三个特性,我们不得不再提一下一个关键字——volatile,它抑制了一些线程运行上的优化,保证了可见性,也一定程度上保证了有序性。复杂的情况则要考虑适当采用synchronized,它在一定意义上保证了这三个特性。
上面这个听起来有点抽象,那么我们可以勉强这样对应一下,主要是有利于我们理解接受,但实际情况可能并不完全有如此清晰的对应关系,好在细节我们不必关注。JVM中每个线程会有自己的栈,而堆是存放各线程所用对象的地方。堆类似于JMM中的主内存,栈中的一部分类似工作内存。
除了有工作内存和主内存的区别外,为了线程的高效并发执行,也是为了配合工作内存或者说缓存的有效利用,实际上会在运行的各个层面上有指令重排序的现象。所以如果在写并发代码的时候,需要把这些考虑进去,保证线程间代码的执行顺序,否则就有可能存在潜在的线程安全问题。在《Java Concurrency in Practice》最后一章中讲到,对JMM的描述还定义了一套偏序规则,通常我们称这个为happens-before规则。这个规则实际上成为了是否符合线程安全要求的最基本依据,也是在重排序中的一个默认保证:
Program order rule. 线程内的代码能够保证执行的先后顺序
Monitor lock rule. 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前
Volatile variable rule. 保证前一个对volatile的写操作在后一个volatile的读操作之前
Thread start rule. 一个线程内的任何操作必需在这个线程的start()调用之后
Thread termination rule. 一个线程的所有操作都会在线程终止之前
Interruption rule. 要保证interrupt()的调用在中断检查之前发生
Finalizer rule. 一个对象的终结操作的开始必需在这个对象构造完成之后
Transitivity. 可传递性
这个虽然说只有8条规则,但对这些顺序规则的真正理解还要在实践中不断体会才行,这里仅仅是一个整理和介绍。
现在再让我们回头来看本片开头的例子,其中对对象的初始化和赋值是不能保证有序的,所以很有可能赋值发生在对象完整构造好之前,这样instance就不为null,而另一个线程得到了这个不为null而又没有被完全初始化的对象,导致问题的存在。
这是个经典的问题,这类问题被称为双重检查锁定(DCL)问题,在《Java Concurrency in Practice》一书的最后谈到了这个问题,并指出这种“优化”是丑陋的,不赞成使用。而建议使用如下的代码替代之:
public class EagerInitialization {
private static Resource resource = new Resource();
public static Resource getResource() { }
如果非要做延迟初始化的话,可以用类似如下的方式:
public class ResourceFactory {
private static class ResourceHolder {
public static Resource resource = new Resource();
public static Resource getResource() {
return ResourceHolder.
可以看到静态内部类ResourceHolder是专门为了处理这个问题而写的,这个类只有首次被用到的时候才会被JVM加载和初始化,做到了延迟初始化。
还有一种说法是在JDK1.5之后,可以用volatile修饰实例属性,但本人未尝试使用过,不能确定。关于更多DCL问题的讨论,可参见这里:
本文到此为止,关于锁和协同的问题,下篇继续讨论。
相关文章:2013 年 8 月 1 日 -- 2013 年 8 月 13 日 -- 2013 年 8 月 28 日 -- 2013 年 8 月 31 日 --
此条目发表在 , , , ,
分类目录,贴了 , , , , , ,
标签。将加入收藏夹。go-goroutine中的共享变量
| Go语言中文网 | Golang中文社区 | Golang中国
<meta name="author" content="polaris ">
go-goroutine中的共享变量
go-goroutine中的共享变量
lmxmimihuhu
转自 : /sevenyuan/archive//3029388.html
Happens Before
对于一个goroutine来说,虽然指令会被编译器乱序重排,但它其中变量的读, 写操作执行表现必须和从所写的代码得出的预期是一致的。但是在两个不同的goroutine对相同变量操作时,
可能因为指令重排导致不同的goroutine对变量的操作顺序的认识变得不一致。为了解决这种二义性问题,Go语言中引进一个happens before的概念,它用于描述对内存操作的先后顺序问题。如果事件e1
happens before 事件 e2,事件e2 happens after e1。如果,事件e1 does not happen before 事件 e2,并且 does not happen after e2,事件e1和e2同时发生。
对于一个单一的goroutine,happens before 的顺序和代码的顺序是一致的。
为了保证读事件r可以感知对变量v的写事件,我们首先要确保w是变量v的唯一的写事件。同时还要满足以下条件:
“写事件w” happens before “读事件r”。其他对变量v的访问必须 happens before “写事件w” 或者 happens after “读事件r”。
第二组条件比第一组条件更加严格。因为,它要求在w和 r并行执行的程序中不能再有其他的读操作。对于在单一的goroutine中两组条件是等价的,读事件可以确保感知到对变量的写事件。但是,对于在 两个goroutines共享变量v,我们必须通过同步事件来保证 happens-before 条件 (这是读事件感知写事件的必要条件)。
同步(Synchronization)
程序的初始化在一个独立的goroutine中执行。初始化goroutine执行完成后,启动过程中建立的其他goroutine会自动启动。
然后会执行init初始化函数。如果包p导入了包q,包q的init初始化函数将在包p的初始化之前执行。init中创建的goroutine会在init结束后执行。
init结束后,程序的入口函数 main.main 启动。
Goroutine的创建
例如,下面的程序:
func f() {
func hello() {
a = &#34;hello, world&#34;;
调用hello函数,会在某个时刻打印“hello, world”(有可能是在hello函数返回之后)。
Channel communication 管道通信
用管道通信是两个goroutines之间同步的主要方法。通常的用法是不同的goroutines对同一个管道进行读写操作,一个goroutines写入到管道中,另一个goroutines从管道中读数据。
管道上的发送操作发生在管道的接收完成之前(happens before)。
例如这个程序:
var c = make(chan int, 10)
var a string
func f() {
a = &#34;hello, world&#34;;
func main() {
可以确保会输出&#34;hello, world&#34;。因为,a的赋值发生在向管道 c发送数据之前,而管道的发送操作在管道接收完成之前发生。因此,在print 的时候,a已经被赋值。
从一个unbuffered管道接收数据在向管道发送数据完成之前发送。
下面的是示例程序:
var c = make(chan int)
var a string
func f() {
a = &#34;hello, world&#34;;
func main() {
同样可以确保输出“hello, world”。因为,a的赋值在从管道接收数据 前发生,而从管道接收数据操作在向unbuffered 管道发送完成之前发生。所以,在print 的时候,a已经被赋值。如果用的是缓冲管道(如 c
= make(chan int, 1) ),将不能保证输出 “hello, world”结果(可能会是空字符串,但肯定不会是他未知的字符串, 或导致程序崩溃)。
包sync实现了两种类型的锁: sync.Mutex 和 sync.RWMutex。
对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n & m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。
例如程序:
var l sync.Mutex
var a string
func f() {
a = &#34;hello, world&#34;;
l.Unlock();
func main() {
可以确保输出“hello, world”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。
For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n&#39;th call to l.Unlock and the matching l.RUnlock happens before the n+1&#39;th call to l.Lock.
包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。
var a string
func setup() {
a = &#34;hello, world&#34;;
func doprint() {
once.Do(setup);
func twoprint() {
go doprint();
go doprint();
调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。
错误的同步方式
注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。
var a, b int
func f() {
func g() {
func main() {
函数g可能输出2,也可能输出0。
这种情形使得我们必须回避一些看似合理的用法。
这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:
var a string
var done bool
func setup() {
a = &#34;hello, world&#34;;
func doprint() {
if !done {
once.Do(setup);
func twoprint() {
go doprint();
go doprint();
在doprint函数中,写done暗示已经给a赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。
另一个错误陷阱是忙等待:
var a string
var done bool
func setup() {
a = &#34;hello, world&#34;;
func main() {
go setup();
for !done {
我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。
下面的用法本质上也是同样的问题.
type T struct {
func setup() {
t := new(T);
t.msg = &#34;hello, world&#34;;
func main() {
go setup();
for g == nil {
print(g.msg);
即使main观察到了 g != nil 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。
在这些例子中,只有一种解决方法:用显示的同步。
支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
支持 @ 本站用户;支持表情(输入 : 提示),见
记住登录状态
还不是会员Java多个线程之间处理共享数据的方式_Linux编程_Linux公社-Linux系统门户网站
你好,游客
Java多个线程之间处理共享数据的方式
来源:Linux社区&
作者:wjw0130
有4个线程,其中两个线程每次对x加1,另外两个每次对x减1,如何实现?
x就是这4个线程要处理的共享数据,不同种线程有不同的处理方式,但操作的数据是共同的,联想到&窗口买票问题&,但是卖票只是单纯的减。
所以,多个线程访问共享对象或数据的方式,先看,如果每个线程执行的代码相同,那么就可以使用同一个Runnable对象,这个对象中有被共享的数据,如买票问题。
如果每个线程执行的代码不同,肯定需要不用的Runnable对象,有两种方式实现这些不同的Runnable对象之间的数据共享:
第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的Runnable,每个线程对共享数据的操作也分给那个
对象完成,这样就可以实现对该数据处理的互斥与通信。
class ShareData {&private int x = 0;
&public synchronized void addx(){&& &x++;& &System.out.println("x++ : "+x);& &}&public synchronized void subx(){&& &x--;& &System.out.println("x-- : "+x);& &&}}class MyRunnable1 implements Runnable{&private ShareData share1 =&public MyRunnable1(ShareData share1) {& this.share1 = share1;& &}&public void run() {& for(int i = 0;i&100;i++){& share1.addx();& }&}}class MyRunnable2 implements Runnable{&private ShareData share2 =&public MyRunnable2(ShareData share2) {& this.share2 = share2;& &}&public void run() {& for(int i = 0;i&100;i++){& share2.subx();& }&}}public class ThreadsVisitData {
&public static void main(String[] args) {& ShareData share = new ShareData();& new Thread(new MyRunnable1(share)).start();& new Thread(new MyRunnable2(share)).start();&&}}
将这些Runnable对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作
class ShareData {&private int x = 0;
&public synchronized void addx(){& &x++;& &System.out.println("x++ : "+x);&}&public synchronized void subx(){& &x--;& &System.out.println("x-- : "+x);&}}
public class ThreadsVisitData {
& public static ShareData share = new ShareData();& &public static void main(String[] args) {& //final ShareData share = new ShareData();& new Thread(new Runnable() {&& &public void run() {& & for(int i = 0;i&100;i++){& & share.addx();& & }& &}& &}).start();& new Thread(new Runnable() {&& & public void run() {& & &for(int i = 0;i&100;i++){& & &share.subx();& & &}& & }& &}).start(); &}}
总结:要同步互斥的代码任务最好将他们分别放在独立的方法中,这些方法再放在同一个类中,这样比较容易实现操作的同步和通信。
Java多线程从简单到复杂
Java多线程经典案例
Java多线程:ReentrantReadWriteLock读写锁的使用
Java内存映射文件实现多线程下载
Java多线程:一道阿里面试题的分析与应对
Java中两种实现多线程方式的对比分析
本文永久更新链接地址:
相关资讯 & & &
& (01月02日)
& (11/29/:56)
& (01月08日)
& (12/22/:34)
& (11/07/:39)
   同意评论声明
   发表
尊重网上道德,遵守中华人民共和国的各项有关法律法规
承担一切因您的行为而直接或间接导致的民事或刑事法律责任
本站管理人员有权保留或删除其管辖留言中的任意内容
本站有权在网站内转载或引用您的评论
参与本评论即表明您已经阅读并接受上述条款
z 发表于 没有考虑互锁情况3633人阅读
Java(43)
以前转载了Java内存模型的系列文章
下面这篇文章也可以对比着看看,加深对JMM的了解。
==========================================================
学习Java并发,到后面总会接触到happens-before偏序关系。初接触玩意儿简直就是不知所云,下面是经过一段时间折腾后个人对此的一点浅薄理解,希望对初接触的人有帮助。如有不正确之处,欢迎指正。
synchronized、大部分锁,众所周知的一个功能就是使多个线程互斥/串行的(共享锁允许多个线程同时访问,如读锁)访问临界区,但他们的第二个功能 —— 保证变量的可见性 —— 常被遗忘。
为什么存在可见性问题?简单介绍下。相对于内存,CPU的速度是极高的,如果CPU需要存取数据时都直接与内存打交道,在存取过程中,CPU将一直空闲,这是一种极大的浪费,妈妈说,浪费是不好的,所以,现代的CPU里都有很多寄存器,多级cache,他们比内存的存取速度高多了。某个线程执行时,内存中的一份数据,会存在于该线程的工作存储中(working memory,是cache和寄存器的一个抽象,这个解释源于《Concurrent Programming in Java: Design Principles
and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有不少人觉得working memory是内存的某个部分,这可能是有些译作将working memory译为工作内存的缘故,为避免混淆,这里称其为工作存储,每个线程都有自己的工作存储),并在某个特定时候回写到内存。单线程时,这没有问题,如果是多线程要同时访问同一个变量呢?内存中一个变量会存在于多个工作存储中,线程1修改了变量a的&#20540;什么时候对线程2可见?此外,编译器或运行时为了效率可以在允许的时候对指令进行重排序,重排序后的执行顺序就与代码不一致了,这样线程2读取某个变量的时候线程1可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的。这就是可见性问题的由来。
我们无法枚举所有的场景来规定某个线程修改的变量何时对另一个线程可见。但可以制定一些通用的规则,这就是happens-before。它是一个偏序关系,Java内存模型中定义了许多Action,有些Action之间存在happens-before关系(并不是所有Action两两之间都有happens-before关系)。“ActionA happens-before ActionB”这样的描述很扰乱视线,是不是?OK,换个描述,如果ActionA happens-before ActionB,我们可以记作hb(ActionA,ActionB)或者记作ActionA
& ActionB,这货在这里已经不是小于号了,它是偏序关系,是不是隐约有些离散数学的味道,不喜欢?嗯,我也不喜欢,so,下面都用hb(ActionA,ActionB)这种方式来表述。
从Java内存模型中取两条happens-before关系来瞅瞅:
An unlock on a monitor happens-before every subsequent lock on that monitor.A write to a volatile field happens-before every subsequent read of that volatile.
“对一个monitor的解锁操作happens-before后续对同一个monitor的加锁操作”、“对某个volatile字段的写操作happens-before后续对同一个volatile字段的读操作”……莫名其妙、不知所云、不能理解……就是这个心情。是不是说解锁操作要先于锁定操作发生?这有违常规啊。确实不是这么理解的。happens-before规则不是描述实际操作的先后顺序,它是用来描述可见性的一种规则,下面我给上述两条规则换个说法:
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。如果线程1写入了volatile变量v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。
是不是很简单,瞬间觉得这篇文章弱爆了,说了那么多,其实就是在说“如果hb(a,b),那么a及之前的写操作在另一个线程t1进行了b操作时都对t1可见(同一个线程就不会有可见性问题,下面不再重复了)”。虽然弱爆了,但还得有始有终,是不是,继续来,再看两条happens-before规则:
All actions in a thread happen-before any other thread successfully returns from a join() on that thread.Each action in a thread happens-before every subsequent action in that thread.
线程t1写入的所有变量(所有action都与那个join有hb关系,当然也包括线程t1终止前的最后一个action了,最后一个action及之前的所有写入操作,所以是所有变量),在任意其它线程t2调用t1.join()成功返回后,都对t2可见。线程中上一个动作及之前的所有写操作在该线程执行下一个动作时对该线程可见(也就是说,同一个线程中前面的所有写操作对后面的操作可见)
大致都是这个样子的解释。
happens-before关系有个很重要的性质,就是传递性,即,如果hb(a,b),hb(b,c),则有hb(a,c)。
Java内存模型中只是列出了几种比较基本的hb规则,在Java语言层面,又衍生了许多其他happens-before规则,如ReentrantLock的unlock与lock操作,又如AbstractQueuedSynchronizer的release与acquire,setState与getState等等。
接下来用hb规则分析两个实际的可见性例子。
看个CopyOnWriteArrayList的例子,代码中的list对象是CopyOnWriteArrayList类型,a是个静态变量,初始&#20540;为0
假设有以下代码与执行线程:
list.set(1,&t&);
list.get(0);
那么,线程2中b的&#20540;会是1吗?来分析下。假设执行轨迹为以下所示:
p2:list.set(1,&t&)
p3:list.get(2)
p4:int b =
p1,p2是同一个线程中的,p3,p4是同一个线程中的,所以有hb(p1,p2),hb(p3,p4),要使得p1中的赋&#20540;操作对p4可见,那么只需要有hb(p1,p4),前面说过,hb关系具有传递性,那么若有hb(p2,p3)就能得到hb(p1,p4),p2,p3是不是存在hb关系?翻翻,发现有如下描述:
Actions in a thread prior to placing an object into any concurrent collection happen-before actions subsequent to the access or removal of that element from the collection in another thread.
p2是放入一个元素到并发集合中,p3是从并发集合中取,符合上述描述,因此有hb(p2,p3).也就是说,在这样一种执行轨迹下,可以保证线程2中的b的&#20540;是1.如果是下面这样的执行轨迹呢?
p3:list.get(2)
p2:list.set(1,&t&)
p4:int b =
依然有hb(p1,p2),hb(p3,p4),但是没有了hb(p2,p3),得不到hb(p1,p4),虽然线程1给a赋&#20540;操作在执行顺序上是先于线程2读取a的,但jmm不保证最后b的&#20540;是1.这不是说一定不是1,只是不能保证。如果程序里没有采取手段(如加锁等)排除类&#20284;这样的执行轨迹,那么是无法保证b取到1的。像这样的程序,就是没有正确同步的,存在着数据争用(data race)。
既然提到了CopyOnWriteArrayList,那么顺便看下其set实现吧:
* Replaces the element at the specified position in this list with the
* specified element.
* @throws IndexOutOfBoundsException {@inheritDoc}
public E set(int index, E element) {
final ReentrantLock lock = this.
lock.lock();
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] =
setArray(newElements);
// Not quite a no- ensures volatile write semantics
setArray(elements);
return oldV
} finally {
lock.unlock();
&#160; &#160; &#160;* Gets the array. &#160;Non-private so as to also be accessible
&#160; &#160; &#160;* from CopyOnWriteArraySet class.
&#160; &#160; &#160;*/
&#160; &#160; final Object[] getArray() {
&#160; &#160; &#160; &#160;
&#160; &#160; }
&#160; &#160; /**
&#160; &#160; &#160;* Sets the array.
&#160; &#160; &#160;*/
&#160; &#160; final void setArray(Object[] a) {
&#160; &#160; &#160; &#160; array =
&#160; &#160; }
&/pre&&p&&/p&&p&有意思的地方是else里的setArray(elements)调用,看看setArray做了什么:一个简单的赋值,array是volatile类型。elements是从getArray()方法取过来的,getArray()实现也很简单,直接返回array。取得array,又重新赋值给array,有甚意义?setArray(elements)上有条简单的注释,但可能不是太容易明白。正如前文提到的那条javadoc上的规定,放入一个元素到并发集合与从并发集合中取元素之间要有hb关系。set是放入,get是取(取还有其他方法),怎么才能使得set与get之间有hb关系,set方法的最后有unlock操作,如果get里有对这个锁的lock操作,那么就好满足了,但是get并没有加锁:&/p&&p&&/p&&pre name=&code& class=&java&&
public E get(int index) {
return get(getArray(), index);
但是get里调用了getArray,getArray里有读volatile的操作,只需要set走任意代码路径都能遇到写volatile操作就能满足条件了,这里主要就是if…else…分支,if里有个setArray操作,如果只是从单线程角度来说,else里的setArray(elements)是没有必要的,但是为了使得走else这个代码路径时也有写volatile变量操作,就需要加一个setArray(elements)调用。
最后,以FutureTask结尾,这应该是个比较有名的例子了,随提一下。提交任务给线程池,我们可以通过FutureTask来获取线程的运行结果。绝大部分时候,将结果写入FutureTask的线程和读取结果的不会是同一个线程。写入结果和获取结果的代码如下:
V innerGet(long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException {
if (!tryAcquireSharedNanos(0, nanosTimeout))
throw new TimeoutException();
if (getState() == CANCELLED)
throw new CancellationException();
if (exception != null)
throw new ExecutionException(exception);
void innerSet(V v) {
for (;;) {
int s = getState();
if (s == RAN)
if (s == CANCELLED) {
// aggressively release to set runner to null,
// in case we are racing with a cancel request
// that will try to interrupt runner
releaseShared(0);
if (compareAndSetState(s, RAN)) {
releaseShared(0);
结果就是result变量,但result不是volatile变量,而这里有没有加锁操作,那么怎么保证写入到result的&#20540;对读取result的线程可见?这里是经过精心设计的,因为读写volatile的开销很小,但毕竟还是存在开销的,且作为一个基础类库,追求最后一点性能也不为过,因为无法预知所有可能的使用场景。这里主要利用了AbstractQueuedSynchronizer中的releaseShared与tryAcquireSharedNanos存在hb关系。
p1:result =
p2:releaseShared(0);
p3:tryAcquireSharedNanos(0, nanosTimeout)
正如前面分析的那样,在这个执行轨迹中,有hb(p1,p2),hb(p3,p4)且有hb(p2,p3),所有有hb(p1,p4),因此,即使result是普通变量,p1中的写操作也是对p4可见的。但,会不会存在这样的轨迹呢:
p1:result =
p3:tryAcquireSharedNanos(0, nanosTimeout)
p2:releaseShared(0);
这也是一个关键点所在,这种情况是决计不会发生的。因为如果没有p2操作,那么p3在执行tryAcquireSharedNanos时会一直被阻塞,直到releaseShared操作执行了或超过了nanosTimeout超时时间或被中断抛出InterruptedException,若是releaseShared执行了,则就变成了第一个轨迹,若是超时,那么返回&#20540;是false,代码逻辑中就直接抛出了异常,不会去取result了,所以,这个地方设计的很精巧。这就是所谓的“捎带同步(piggybacking
on synchronization)”,即,没有特意为result变量的读写设置同步,而是利用了其他同步动作时“捎带”的效果。但在我们自己写代码时,应该尽可能避免这样的做法,因为,不好理解,对编码人员要求高,维护难度大。
本文只是简单地解释了下hb规则,文中还出现了许多名词没有做更多介绍,为啥没介绍?介绍开来就是一本书啦,他们就是《Java Memory Model》、《Java Concurrency in Practice》、《Concurrent Programming in Java: Design Principles and Patterns》等,这些书里找定义与解释吧。
原创文章,转载请注明: 转载自
本文链接地址:
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:932156次
积分:8984
积分:8984
排名:第1644名
原创:116篇
转载:95篇
评论:135条
(9)(9)(1)(19)(3)(13)(9)(4)(9)(6)(1)(1)(5)(9)(2)(1)(1)(1)(3)(4)(2)(6)(1)(2)(6)(3)(40)(6)(8)(4)(1)(4)(1)(7)(2)(3)(3)(2)

我要回帖

更多关于 python多线程共享变量 的文章

 

随机推荐