http://www.importnew.com/24082.html volatile关键字html
http://www.importnew.com/16142.html ConcurrentHashMap原理分析java
http://www.importnew.com/19612.html Java内存模型c++
Java内存模型:编程
关键字:主存、工做内存;堆区、栈区(http://www.importnew.com/19612.html )缓存
在Java Memory Model中,Memory分为两类,main memory和working memory,main memory为全部线程共享,working memory中存放的是线程所须要的变量的拷贝(线程要对main memory中的内容进行操做的话,首先须要拷贝到本身的working memory,通常为了速度,working memory通常是在cpu的cache中的)。volatile的变量在被操做的时候不会产生working memory的拷贝,而是直接操做main memory,固然volatile虽然解决了变量的可见性问题,但没有解决变量操做的原子性的问题,这个还须要synchronized或者CAS相关操做配合进行。多线程
Java内存模型规定了全部的变量都存储在主内存中。每条线程中还有本身的工做内存,线程的工做内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的全部操做(读取,赋值)都必须在工做内存中进行。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。并发
并发编程的三大概念:原子性,有序性,可见性。app
也就说假设一个对象中有一个变量i,那么i是保存在main memory中的,当某一个线程要操做i的时候,首先须要从main memory中将i 加载到这个线程的working memory中,这个时候working memory中就有了一个i的拷贝,这个时候此线程对i的修改都在其working memory中,直到其将i从working memory写回到main memory中,新的i的值才能被其余线程所读取。从某个意义上说,可见性保证了各个线程的working memory的数据的一致性。 可见性遵循下面一些规则:函数
还拿上面的例子来讲,原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其余线程对i进行任何操做。也就是说保证某个线程对i的操做是原子性的,这样就能够避免数据脏读。 经过锁机制或者CAS(Compare And Set 须要硬件CPU的支持)操做能够保证操做的原子性。性能
假设在main memory中存在两个变量i和j,初始值都为0,在某个线程A的代码中依次对i和j进行自增操做(i,j的操做不相互依赖)
1
2
|
i++;
j++;
|
因为,因此i,j修改操做的顺序可能会被从新排序。那么修改后的ij写到main memory中的时候,顺序可能就不是按照i,j的顺序了,这就是所谓的reordering,在单线程的状况下,当线程A运行结束的后i,j的值都加1了,在线程本身看来就好像是线程按照代码的顺序进行了运行(这些操做都是基于as-if-serial语义的),即便在实际运行过程当中,i,j的自增可能被从新排序了,固然计算机也不能帮你乱排序,存在上下逻辑关联的运行顺序确定仍是不会变的。可是在多线程环境下,问题就不同了,好比另外一个线程B的代码以下
1
2
3
|
if
(j==
1
) {
System.out.println(i);
}
|
按照咱们的思惟方式,当j为1的时候那么i确定也是1,由于代码中i在j以前就自增了,但实际的状况有可能当j为1的时候i仍是为0。这就是reordering产生的很差的后果,因此咱们在某些时候为了不这样的问题须要一些必要的策略,以保证多个线程一块儿工做的时候也存在必定的次序。JMM提供了happens-before 的排序策略。这样咱们能够获得多线程环境下的as-if-serial语义。 这里不对happens-before进行详细解释了,详细的请看这里http://www.ibm.com/developerworks/cn/java/j-jtp03304/,这里主要讲一下volatile在新的java内存模型下的变化,在jsr133以前,下面的代码可能会出现问题
1
2
3
4
5
6
7
8
9
10
11
12
|
Map configOptions;
char
[] configText;
volatile
boolean
initialized =
false
;
// In Thread A
configOptions =
new
HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized =
true
;
// In Thread B
while
(!initialized)
sleep();
// use configOptions
|
jsr133以前,虽然对 volatile 变量的读和写不能与对其余 volatile 变量的读和写一块儿从新排序,可是它们仍然能够与对 nonvolatile 变量的读写一块儿从新排序,因此上面的Thread A的操做,就可能initialized变成true的时候,而configOptions尚未被初始化,因此initialized先于configOptions被线程B看到,就产生问题了。
JSR 133 Expert Group 决定让 volatile 读写不能与其余内存操做一块儿从新排序,新的内存模型下,若是当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的全部变量值如今均可以保证对 B 是可见的。
结果就是做用更大的 volatile 语义,代价是访问 volatile 字段时会对性能产生更大的影响。这一点在ConcurrentHashMap中的统计某个segment元素个数的count变量中使用到了。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:
1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。
2)禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
1
2
3
4
5
6
7
8
|
//线程1
boolean
stop =
false
;
while
(!stop){
doSomething();
}
//线程2
stop =
true
;
|
这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会彻底运行正确么?即必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。
下面解释一下这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。
那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。
可是用volatile修饰以后就变得不同了:
第一:使用volatile关键字会强制将修改的值当即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(固然这里包括2个操做,修改线程2工做内存中的值,而后将修改后的值写入内存),会使得线程1的工做内存中缓存变量stop的缓存行无效,而后线程1读取时,发现本身的缓存行无效,它会等待缓存行对应的主存地址被更新以后,而后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
下面看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
Test {
public
volatile
int
inc =
0
;
public
void
increase() {
inc++;
}
public
static
void
main(String[] args) {
final
Test test =
new
Test();
for
(
int
i=
0
;i<
10
;i++){
new
Thread(){
public
void
run() {
for
(
int
j=
0
;j<
1000
;j++)
test.increase();
};
}.start();
}
while
(Thread.activeCount()>
1
)
//保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
|
你们想一下这段程序的输出结果是多少?也许有些朋友认为是10000。可是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操做,因为volatile保证了可见性,那么在每一个线程中对inc自增完以后,在其余线程中都能看到修改后的值啊,因此有10个线程分别进行了1000次操做,那么最终inc的值应该是1000*10=10000。
这里面就有一个误区了,volatile关键字能保证可见性没有错,可是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,可是volatile没办法保证对变量的操做的原子性。
在前面已经提到过,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:
假如某个时刻变量inc的值为10,
线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;
而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,也不会致使主存中的值刷新,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。
而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。
那么两个线程分别进行了一次自增操做后,inc只增长了1。
根源就在这里,自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。
解决方案:能够经过synchronized或lock,进行加锁,来保证操做的原子性。也能够经过AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做类,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是原子性操做。atomic是利用CAS来实现原子性操做的(Compare And Swap),CAS其实是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操做。
在前面提到volatile关键字能禁止指令重排序,因此volatile能在必定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;
2)在进行指令优化时,不能将在对volatile变量的读操做或者写操做的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
可能上面说的比较绕,举个简单的例子:
1
2
3
4
5
6
7
8
|
//x、y为非volatile变量
//flag为volatile变量
x =
2
;
//语句1
y =
0
;
//语句2
flag =
true
;
//语句3
x =
4
;
//语句4
y = -
1
;
//语句5
|
因为flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句一、语句2前面,也不会讲语句3放到语句四、语句5后面。可是要注意语句1和语句2的顺序、语句4和语句5的顺序是不做任何保证的。
而且volatile关键字能保证,执行到语句3时,语句1和语句2一定是执行完毕了的,且语句1和语句2的执行结果对语句三、语句四、语句5是可见的。
那么咱们回到前面举的一个例子:
1
2
3
4
5
6
7
8
9
|
//线程1:
context = loadContext();
//语句1
inited =
true
;
//语句2
//线程2:
while
(!inited ){
sleep()
}
doSomethingwithconfig(context);
|
前面举这个例子的时候,提到有可能语句2会在语句1以前执行,那么久可能致使context还没被初始化,而线程2中就使用未初始化的context去进行操做,致使程序出错。
这里若是用volatile关键字对inited变量进行修饰,就不会出现这种问题了,由于当执行到语句2时,一定能保证context已经初始化完毕。
处理器为了提升处理速度,不直接和内存进行通信,而是将系统内存的数据独到内部缓存后再进行操做,但操做完后不知何时会写到内存。
若是对声明了volatile变量进行写操做时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了若是有其余线程对声明了volatile变量进行修改,则当即更新主内存中数据。
但这时候其余处理器的缓存仍是旧的,因此在多处理器环境下,为了保证各个处理器缓存一致,每一个处理会经过嗅探在总线上传播的数据来检查 本身的缓存是否过时,当处理器发现本身缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做时,会强制从新从系统内存把数据读处处理器缓存里。 这一步确保了其余线程得到的声明了volatile变量都是从主内存中获取最新的。
Lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些状况下性能要优于synchronized,可是要注意volatile关键字是没法替代synchronized关键字的,由于volatile关键字没法保证操做的原子性。一般来讲,使用volatile必须具有如下2个条件:
1)对变量的写操做不依赖于当前值
2)该变量没有包含在具备其余变量的不变式中
下面列举几个Java中使用volatile的几个场景。
①.状态标记量
1
2
3
4
5
6
7
8
9
|
volatile
boolean
flag =
false
;
//线程1
while
(!flag){
doSomething();
}
//线程2
public
void
setFlag() {
flag =
true
;
}
|
根据状态标记,终止线程。
②.单例模式中的double check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
Singleton{
private
volatile
static
Singleton instance =
null
;
private
Singleton() {
}
public
static
Singleton getInstance() {
if
(instance==
null
) {
synchronized
(Singleton.
class
) {
if
(instance==
null
)
instance =
new
Singleton();
}
}
return
instance;
}
}
|
主要在于instance = new Singleton()这句,这并不是是一个原子操做,事实上在 JVM 中这句话大概作了下面 3 件事情:
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,被线程二抢占了,这时 instance 已是非 null 了(但却没有初始化),因此线程二会直接返回 instance,而后使用,而后瓜熟蒂落地报错。