以下是两个我提出的样例问题。我关心的并不是特定的答案,而是需要了解答案是如何从规范中得出的(或者我如何得出规范没有答案的结论)。
- "volatile"到底是做什么用的?
- 变量的写入是否是原子性的?这是否取决于变量的类型?
我不打算在这里试图回答你的问题 - 相反,我会将你重定向到一本书,这本书被认为是关于这个主题建议的:《Java并发实践》。
有一个警告:如果这里确实有答案,那么预计其中很多都是错误的。我不发表详细信息的原因之一是因为我非常确定我至少在某些方面上会出错。当我说每个人都认为他们可以回答这个问题实际上具有足够严谨性获得正确答案的机会几乎为零时,我绝不想不尊重社区。(Joe Duffy最近发现了.NET内存模型的一部分,他感到很惊讶。如果他会犯错,我们这样的凡人也会。)
我将提供一些洞见,因为它经常被误解:
易变性和原子性有所区别。人们经常认为原子写入是易变的(即,如果写入是原子的,就不需要担心内存模型)。这是不正确的。
易变性是指一个线程执行读取操作(在源代码中逻辑上)是否会“看到”其他线程所做的更改。
原子性是指是否存在任何可能,如果发生更改,则只会看到更改的一部分。
例如,对整数字段进行写入是保证原子性的,但不是易变的。这意味着如果我们有以下代码(从foo.x = 0开始):
Thread 1: foo.x = 257;
Thread 2: int y = foo.x;
对于变量y
,它可以是0或257。由于原子性约束,它不可能是其他任何值(例如256或1)。但是,即使您知道线程2中的代码在“墙上时间”中在线程1中的代码之后执行,仍可能存在奇怪的缓存、内存访问“移动”等情况。将变量x
定义为易失性将解决这个问题。
其余部分交给真正的专家们。
这是一个很好的链接,可以为您提供一些深入的信息:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
JVM内存模型
高级别图示
代码示例
class MainClass {
void method1() { //<- main
int variable1 = 1;
Class1 variable2 = new Class1();
variable2.method2();
}
}
class Class1 {
static Class2 classVariable4 = new Class2();
int instanceVariable5 = 0;
Class2 instanceVariable6 = new Class2();
void method2() {
int variable3 = 3;
}
}
class Class2 { }
*注意:
线程堆栈
只包含本地变量堆
上"volatile"到底是什么作用?
对变量的写操作是原子的吗?这取决于变量的类型吗?
以上其他答案都是正确的,因为你的问题并不是那么容易解决。
然而,我理解你想要深入了解底层的痛苦 - 对此,我会指向世界上的编译器和 Java 的低级前身 - 即汇编语言、C 和 C++。
阅读有关不同类型障碍(“栅栏”)的文章。了解什么是内存栅栏,以及何时需要它,将帮助您直观地掌握 volatile 的作用。
这是我从这里和其他来源理解到的事情的另一次总结尝试(第一次尝试相当离谱,希望这次更好)。
Java内存模型是关于将一个线程写入内存的值传播到其他线程,以便其他线程在从内存读取时可以看到它们。
简而言之,如果您在互斥锁上获得锁定,则在此之前释放该互斥锁的任何线程写入的任何内容都将对您的线程可见。
如果您读取一个易失变量,则在您读取它之前写入该易失变量的任何内容都将对读取线程可见。此外,在写入您的变量之前写入您的变量的线程执行的任何对易失变量的写入也是可见的。此外,在Java 1.5中,任何写入易失或非易失变量的任何线程发生在写入易失变量之前,都将对您可见。
在构造对象后,您可以将其传递给另一个线程,并且所有最终成员都将在新线程中可见并完全构造。对于非最终成员没有类似的保证。这使我认为对最终成员的赋值就像对易失变量(内存屏障)的写入。
在其Runnable退出之前,线程写入的任何内容都可被执行join()的线程看到。 在执行start()之前,线程写入的任何内容将对生成的线程可见。
另一个需要提及的事情是:volatile变量和同步具有一个很少被提到的功能:除了刷新线程缓存并提供一次只访问外,它们还可以防止编译器和CPU在同步边界上重新排序读取和写入。
这些都不是新鲜事物,其他答案已经更好地阐述了它。 我只是想写下来以清理我的思路。
这是用城市(线程)和星球(主存)来解释的。
http://mollypages.org/tutorials/javamemorymodel.mp
城市之间没有直达航班。
你必须先去另一个星球(在这个例子中是火星),然后到家乡星球的另一个城市。所以,从纽约到东京,你必须经过以下步骤:
纽约 -> 火星 -> 东京
现在用2个线程替换纽约和东京,用主内存替换火星,将获取/释放锁定作为航班,这就是Java内存模型。