可见性
高速缓存容量比主存内存小,但是速度快,所以为了提高执行效率,CPU与主存之间就多了缓存层(不同的CPU有不同的缓存层数),由于CPU有多层缓存,导致读到数据可能会过期,线程之间的共享变量的可见性问题就是由于缓存引起的,Java内存模型标准就屏蔽了底层的多层缓存,只抽象成了两层:主内存和工作内存
Java中所有的共享的变量都存储在主内存中,每个线程都有自己的工作内存,工作内存中保存该线程使用到的变量的主内存副本拷贝;线程对变量的读写操作都应该在工作内存中完成;不同线程之间不能相互访问工作内存,交互数据需要通过主内存
下面代码验证可见性
public class Main {
private static int a = 1;
private static int b = 2;
public static void main(String[] args) {
while (true) {
a = 1;
b = 2;
new Thread(() -> {
try {
Thread.sleep(100); //休眠更容易出现该效果
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 3;
b = a;
}).start();
/**
* thread1先运行完毕,thread2再运行, a=3,b=3
* thread2先运行完毕,thread1再运行, a=1,b=2
* thread1和2交叉运行, a=3,b=2
* thread1先运行完毕,但thread2只看到了对b的修改却未看到对a的修改 a=1,b=3
*/
new Thread(() -> {
try {
Thread.sleep(100); //休眠更容易出现该效果
} catch (InterruptedException e) {
e.printStackTrace();
}
if (a == 1 && b == 3) { //条件是a=1,b=3
System.out.println("a=" + a + ",b=" + b); //输出结果却可能是其他值
}
}).start();
}
}
}
Happens-Before
由于操作的有执行顺序,由于操作可以在一个线程之内,也可以是在不同线程之间,所以Java内存模型通过happens-before规则解决跨线程的内存可见性问题,简单的说就是:前一个操作的结果可以被后续操作获取
volatile、synchronized、lock、并非容器、join等待、线程启动等都可以保证可见性,具体Happens-Before规则如下:
- 单线程原则:又由于传递性,一个线程内,后面语句一定能看到前面语句
- 锁操作:加锁之后一定能看到解锁之前的指令,以及解锁之前的其他操作
- volatile变量:使用volatile修饰的变量,所有线程都能立即看到该变量的修改,以及之前的其他操作
- 线程启动:父线程启动子线程,子线程一定能看到启动前父线程的所有指令
- 线程join:线程join等待结束后,一定能看到被等待线程的所有指令
- 中断:线程被其他线程中断,中断检测或中断异常一定能被其他线程看到
- 构造方法:
finalize()
方法一定能看到构造方法中的最后一条指令 - 工具类的Happens-Before原则
- 线程安全容器get一定能看到在此之前的put等存入动作
- CountDownLatch、CyclicBarrier、Semaphore
- Future
- 线程池
volatile关键字
volatile的作用
volatile只能作用于属性字段上
volatile是一种轻量级的同步机制,因为并不会发生上下文切换等行为开销(无锁机制),相应的能力也小,无法做到synchronized那样的原子保护,只有在很有限的场景下才能发挥作用
- 可见性:读取一个volatile变量之前,需要先适相应的本地缓存失效,这样就一定能从主内存中读取到最新的值;修改一个volatile变量会立即刷新到主内存中
- 禁止指令重排序优化:比如解决单例双重锁乱序问题
不适用的场合
非原子性的数据争用
public class Main {
private volatile static int a;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) a++;
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//保证线程1和线程2先执行结束
System.out.println("最终的a=" + a); //结果不一定是20000
}
}
适用场合
纯赋值的原子操作,由于Happens-Before规则,volatile可作为刷新之前变量的触发器
public class Main {
private volatile static boolean flag;
private static String options;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("读取配置文件");
options = "已赋值配置";
System.out.println("读取配置成功");
flag = true; //由于Happens-Before规则,另一个线程也一定会看到options的修改,从而保证线程安全
});
Thread thread2 = new Thread(() -> {
while (!flag) { //未配置好就陷入休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("配置项为:" + options);
});
thread1.start();
thread2.start();
}
}
对比synchronized
volatile相对于synchronized更轻量,如果一个共享变量至始至终只被各个线程赋值,而没有其他操作,那么就可以使用volatile来代替synchronized以及原子变量,因为赋值本身就具有原子性,而volatile又保证了可见性,所以就保证了线程安全
Comments NOTHING