3.1、可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。为确保多个线程之间对内存写入操作的可性,必须使用同步机制
public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThead extends Thread{
public void run(){
while(!ready) Thrad.yield();
System.out.println(number);
}
}
private static void main(String[] args){
new TeaderThread().static();
number=42;
ready=true;
}
}
上述代码在我们理解的范围来看,应该是等42赋值给number之后,ReaderThead.run()的while才会跳出循环输出,但事实可能并非如此,可能会输出0,也可能永远都无法跳出循环,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值 ,这种现象被称为“重排序”.
当主线程首先写入number,然后在没有同步的情况下写入ready那么读线程看到的顺序可能与写入的顺序完全相反
在没有同步的情况下,编译器、处理器以及运行时等都会对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结果
3.1.1、失效数据
失效数据:读取一个已经失效的值,例如NoVisibility的number成员
MutableInteger不是线程安全的,因为get和set都没有在同步的情况下访问value,如果同时访问get和set,可能会导致读取的数据是失效的
public class MutableIteger{
private int value;
public int get(){return value;}
public void set(int value){this.value=value;}
}
通过给get和set方法进行同步可以使MutableIteger变成一个线程安全的类, 同时如果只给set或者get进行同步也是不够的,调用未同步的方法还是会看见失效值
public class MutableIteger{
private int value;
public synchronized int get(){return value;}
public synchronized void set(int value){this.value=value;}
}
3.1.2、非原子的64位操作
最低安全性(out-of-thin-airsafety):最低限度的获得一个安全值,获得某个线程设置的值而不是随机值,可能是以前赋的值
当计算非volatile类型的long和double变量,JVM将64位的读写操作分解成两个32位的操作,如果这两个32位的操作在不同的线程中执行,那么有可能会得到失效数据
3.1.3、加锁和可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步
3.1.4、Volatile变量
java提供了一种稍弱的同步机制(Volatile变量),用来确保将变量的更新操作通知到其他线程。声明为Volatile变量之后,编译器的运行时都会注意到这个变量是共享的,因此不会将该变量上的操作重排序,Volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,所以volatile变量总是返回最新写入的值
volatile变量对可见性的影响比volatile变量本身更为重要,当线程A首先写入一个Volatile变量,线程B随后读取该变量,那么线程A在写入之前的值对A都是可见的,线程B读取了该变量后,对B也是可见的
volatile的一种典型用法:检查某个状态标记以判断是否退出循环
volatile boolean asleep{
...
while(!asleep)
countSomeSheep();
}
volatile的语义不足以确保递增操作(count++)的原子性,除非您确保只有一个线程对变量执行写操作
当且仅当满足以下所有条件时,才应该使用Volatile变量:
1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新 变量的值
2. 该变量不会与其他状态变量一起纳入不变性条件中
3. 在访问变量时不需要加锁