并发编程volatile精解
多线程下变量的不可见性
在多线程并发执行的情况下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量最新值。(多线程下修改共享变量会出现变量修改值后的不可见性)
可见性问题的原因
所有共享变量存在于主内存中,每个线程由自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
解决方法
1、加锁 2、使用volatile关键字(底层实现原理是内存屏障)【并发编程】volatile
volatile修改的变量可以在多线程并发修改下,实现线程间变量的可见性
在Java中,volatile 关键字确实不保证复合操作的原子性,但是对于long和double类型的变量,volatile 修饰的读写操作是原子的。
在多线程环境中,vulatile关键字可以保证共享数据的可见性,但是不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是不安全的)
要保证数据的安全性,还需要使用锁机制。
什么是重排序
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
好处
重排序可以提高处理的速度。
使用volatile可以禁止指令的重排序,从而修正重排序可能带来的并发安全问题。
(7)线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
(8)对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始。
long和double的原子性
在java中,long和double都是8个字节共64位(一个字节=8bit),那么如果是一个32位的系统,读写long或double的 变量时会涉及到原子性问题,因为32位的系统要读完一个64位的变量,需要分两步执行,每次读取32位,这样就对double和long变量的赋值就会出现问题: 如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。
public class LongTest09 implements Runnable{
private volatile static long aLong = 0;
private volatile long value;
public LongTest09(long value) {
this.setValue(value);
}
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
@Override
public void run() {
int i = 0;
while (i < 100000) {
LongTest09.aLong = this.getValue();
i++;
// 赋值操作!!
long temp = LongTest09.aLong;
// 取出值操作!!!
if (temp != 1L && temp != -1L) {
System.out.println("出现错误结果" + temp);
System.exit(0);
}
}
System.out.println("运行正确");
}
public static void main(String[] args) throws InterruptedException {
// 获取并打印当前JVM是32位还是64位的
String sysNum = System.getProperty("sun.arch.data.model");
System.out.println("系统的位数:"+sysNum);
LongTest09 t1 = new LongTest09(1);
LongTest09 t2 = new LongTest09(-1);
Thread T1 = new Thread(t1);
Thread T2 = new Thread(t2);
T1.start();
T2.start();
T1.join();
T2.join();
}
}
上面的代码在32位环境和64位环境执行的结果是不一样的: 32位环境:出现错误结果 原因:32位环境无法一次读取long类型数据,多线程环境下对Long变量的读写是不完整的,导致temp变量既不等于1也不等于-1。出现了long和double读写的原子性问题了。 64位环境:运行正确
如果是在64位的系统中,那么对64位的long和double的读写都是原子操作的。即可以以一次性读写long或double的整个64bit。如果在32位的JVM上,long和double就不是原子性操作了。
解决办法
- 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
- 如果使用volatile修饰long和double,那么其读写都是原子操作
- 在实现JVM时,可以自由选择是否把读写long和double作为原子操作;
- java中对于long和double类型的写操作不是原子操作,而是分成了两个32位的写操作。读操作是否也分成了两 个32位的读呢?在JSR-133之前的规范中,读也是分成了两个32位的读,但是从JSR-133规范开始,即JDK5开始,读操作也都具有原子性;
- java中对于其他类型的读写操作都是原子操作(除long和double类型以外);
- 对于引用类型的读写操作都是原子操作,无论引用类型的实际类型是32位的值还是64位的值;
- Java商业虚拟机已经解决了long和double的读写操作的原子性。
双重检查锁
双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。
public class Singleton {
//静态属性,volatile保证可见性和禁止指令的重排序
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
//第一重检查锁定
if (null == uniqueSingleton) {
//同步代码块
synchronized (Singleton.class) {
//第二重检查锁定
if (null == uniqueSingleton) {
//注意:非原子操作
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
- 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。
- 获取锁。
- 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。
public class UseVolatile1 implements Runnable {
volatile boolean flag = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
flag = true; // 纯赋值操作符合预期
// flag = !flag ; // 这样做不符合预期
}
}
class Test{
public static void main(String[] args) throws InterruptedException {
UseVolatile1 r = new UseVolatile1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(r.flag);
System.out.println(r.realA.get());
}
}
触发器
按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见。
public class VisibilityHP {
int a = 1;
int b = 2;
int c = 3;
volatile boolean flag = false;
private void write() {
a = 3;
b = 4 ;
c = a;
flag = true;
}
private void read() {
// flag被volatile修饰,充当了触发器,一旦值为true,此处立即对变量之前的操作可见。
while(flag){
System.out.println("a=" + a + ";b=" + b +",c="+c );
}
}
public static void main(String[] args) {
while (true) {
VisibilityHP test = new VisibilityHP();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.write();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.read();
}
}).start();
}
}
}
volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的 值后,触发获取到的该变量之前的操作都将是最新的且可见。
volatile与synchronized
volatile总结
原文地址:https://blog.csdn.net/w287586/article/details/143587774
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!