多线程下变量的不可见性
在多线程并发执行的情况下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量最新值。(多线程下修改共享变量会出现变量修改值后的不可见性)
可见性问题的原因
所有共享变量存在于主内存中,每个线程由自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
解决方法
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;}@Overridepublic 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(); @Overridepublic 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() {@Overridepublic void run() { try {Thread.sleep(100);} catch (InterruptedException e) { e.printStackTrace();}test.write(); }}).start();new Thread(new Runnable() { @Overridepublic void run() { try {Thread.sleep(100);} catch (InterruptedException e) { e.printStackTrace();}test.read(); }}).start();}}}
volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的 值后,触发获取到的该变量之前的操作都将是最新的且可见。