了解Java多线程的可见性与有序性
可见性
在Java多线程中,可见性问题是指当多个线程访问共享数据时,其中一个线程对数据进行了修改,导致其他线程无法立即看到这个修改的结果。
原因
可见性问题的产生是因为java内存模型中存在主内存和工作内存的缓存机制,不同的线程可能会将共享数据拷贝到自己的工作内存中进行修改,修改后的结果,在没有及时写回主内存的情况下,其他线程是看不到的。
解决方案
Java提供了volatile关键字来保证多线程间共享变量的可见性。
被volatile修饰的变量,操作之后都会立即写入主内存,而其他线程在进行操作之前,会先访问主内存中的这个变量,从而保证操作都是对主内存的同一个变量进行的,因此能够避免了可见性问题的发生。
public class volatileDemo {
public volatile boolean flag = false;
public void setFlag(){
flag = true;
}
public void printFlag() {
System.out.println("flag:" + flag);
}
public static void main(String[] args) {
final volatileDemo demo = new volatileDemo();
new Thread(new Runnable() {
public void run() {
demo.setFlag();
}
}).start();
new Thread(new Runnable() {
public void run() {
while(!demo.flag) {
System.out.println("flag is false.");
Thread.yield();
}
System.out.println("flag is true.");
}
}).start();
}
}
示例说明
假设有两个线程T1、T2,并且他们共享了一个布尔类型的变量flag,初始值均为false。
在T1线程中,调用setFlag()方法将flag设置为true。
在T2线程中,通过自旋的方式(即反复执行一个简单的操作等待目标操作完成)来读取flag的值,如果值为false,就继续等待,一旦值为true,就输出flag is true.的语句。
如果没有使用volatile修饰flag的话,T2线程可能永远看不到T1线程所做的修改,因为T2线程所读取的flag是从自己的工作内存中获取的。而使用volatile修饰flag的话,相当于保证了T2线程所读取的flag是从主内存中获取的,因此能够保证可见性的问题。
有序性
Java内存模型中的有序性,是指JMM规定的代码执行顺序和程序员编写代码时期望的有序执行顺序不同的问题。
原因
由于JMM允许编译器和处理器对指令进行重排序的优化,因此可能会导致程序员编写的代码执行顺序和实际执行顺序不一致的问题。
例如,代码A中包含修改共享变量和判断共享变量的操作,如果编译器或处理器对代码进行重排序,可能会导致先判断共享变量再修改共享变量的情况出现,因此就会导致程序出现错误的结果。
解决方案
Java为程序员提供了很多手段来保证多线程的有序性:
synchronized
可以使用synchronized关键字来保证多线程的有序性,synchronized具有原子性、可见性、有序性等特性,能够避免编译器和处理器对指令进行重排序的优化。
volatile
被volatile修饰的变量能够保证线程之间的有序性,每个线程在访问volatile变量之前,都会先刷新本地内存中的变量,保证所有的线程都能够访问到最新的变量值。
final关键字
final关键字在多线程中也具有有序性,final域的初始化不会被重排序,并且对于所有线程可见,因此能够保持有序性。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
示例说明
在单例模式中,为了确保线程安全性,getInstance方法需要保证只能同时一个线程进入到创建实例的阶段。
如果不使用volatile修饰instance的话,可能会出现重排序的问题,比如初始化对象的时候分3个步骤:1)分配对象的内存空间;2)初始化对象;3)将对象指向刚分配的内存地址。但是由于2和3的顺序不需要保证,因此可能会出现先将对象指向地址,但是还没有初始化的情况,这样会导致其他访问instance的线程得到的是一个还没初始化的实例,从而导致程序出错。
而使用volatile修饰instance的话,能够避免这种重排序的问题,能够保证有序性的正确性。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:了解Java多线程的可见性与有序性 - Python技术站