Volatile
Volatile
实现原理
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过 嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
1、Lock前缀指令会引起处理器缓存回写到内存
2、一个处理器的缓存回写到内存会导致其他处理器的缓存无效
Volatile的特性
一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都使用同一个锁来同步,他们之间的执行效果相同
public class VolatileFeature {
volatile long vl=0l;
public void set(long l){
vl=l;
}
public void getAndIncrement(){
vl++;
}
public long get(){
return vl;
}
}
等价于
public class VolatileFeature {
long vl=0l;
public synchronized void set(long l){
vl=l;
}
//由于volatile变量的自增操作是一个复合操作,不能保证原子性
public void getAndIncrement(){
long temp=get();
temp+=1l;
set(temp);
}
public synchronized long get(){
return vl;
}
}
volatile写-读的内存语意
当写一个volatile变量时,JMM会把该线程对应的本地内存(工作内存)中的共享变量刷新到贮存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主存中读取共享变量。
使用volatile需要注意的问题
1、volatile关键字不能保证volatile变量复合操作的原子性
public class VolatileCounting {
private static volatile int count=0;
private static void addCount(){
count++;
}
public static void main(String[] args){
int threadCount=1000;
Thread[] threads=new Thread[threadCount];
for(int i=0;i<threadCount;i++){
threads[i]=new Thread(new Runnable(){
@Override
public void run(){
for(int j=0;j<1000;j++){
addCount();
}
}
});
}
for(int i=0;i<threadCount;i++){
threads[i].start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(count);
}
}
输出:996840
2、对64位long和double型变量的非原子性协
java内存模型,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对他们进行读取或修改操作,那么某些线程可能会读取到一个即非原值,也不是其他线程修改值的“中间值”。
long和double占用的字节数都是8,也就是64bits。在64位操作系统上,JVM中double和long的赋值操作是原子操作。但是在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据。这样对double和long的赋值操作就会有问题:如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。因此需要使用volatile关键字来防止此类现象。volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
public class LongVolatile {
private static long value;
private static void set0(){
value=0;
}
private static void set1(){
value=-1;
}
public static void main(String[] args) {
System.out.println(Long.toBinaryString(-1));//-1的64位表示
System.out.println(pad(Long.toBinaryString(0),64));//0的64位表示
Thread t0=new Thread(new Runnable(){
@Override
public void run(){
set0();
}
});
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
set1();
}
});
t0.start();
t1.start();
long temp;
while ((temp = value) == -1 || temp == 0) {
//如果静态成员value的值是-1或0,说明两个线程操作没有交叉
}
System.out.println(pad(Long.toBinaryString(temp), 64));
System.out.println(temp);
t0.interrupt();
t1.interrupt();
}
// 将0扩展
private static String pad(String s, int targetLength) {
int n = targetLength - s.length();
for (int x = 0; x < n; x++) {
s = "0" + s;
}
return s;
}
}
在32位操作系统上,我们开启两个线程,对long类型的共享变量,不停的进行赋值操作,而主线程检测是否产生“中间值”。结果肯呢为
1111111111111111111111111111111111111111111111111111111111111111
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000011111111111111111111111111111111
4294967295
或者
1111111111111111111111111111111111111111111111111111111111111111
0000000000000000000000000000000000000000000000000000000000000000
1111111111111111111111111111111100000000000000000000000000000000
-4294967296
如果我们将变量声明为volatile或者将程序在64位操作系统上运行,那么程序将进入死循环。由于赋值操作具有原子性,不会出现所谓的中间结果。
3、volatile可以禁止重排序
例如,利用volatile可以实现双重检验锁的单例模式。
Synchronized
实现原理
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。本文已经较为详细的介绍了Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现
为以下3种形式。
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
锁的释放和获取的内存语义
当线程释放锁时,JVM会把该线程对应的本地内存(工作内存)中的共享变量刷新到主内存中。
当线程获取锁时,JVM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
对比锁释放-获取的内存语义与volatile写-读的内存语义,可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
final
final域的内存语义
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化了,而普通域不具有这个保障。
读final域的重排序规则可以确保:在读取一个对象的final之前,一定会先读取包含这个final域的对象的引用。
- 点赞
- 收藏
- 关注作者
评论(0)