volatile 关键字,使一个变量在多个线程间可见
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
使用volatile关键字,会让所有线程都会读到变量的修改值
在下面的代码中,running是存在于堆内存的t对象中
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
public class T01_HelloVolatile {
volatile boolean running = true;
void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
-
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
-
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized,synchronized可以保证可见性和原子性,volatile只能保证可见性
-
不要以字符串常量作为锁定对象,m1和m2其实锁定的是同一个对象。这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized (s1) {
}
}
void m2() {
synchronized (s2) {
}
}
- 锁定某对象o,如果o的属性发生改变,不影响锁的使用。但是如果o变成另外一个对象,则锁定的对象发生改变。应该避免将锁定对象的引用变成另外的对象
volite的用途
1.线程可见性
package com.mashibing.testvolatile;
public class T01_ThreadVisibility {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
while (flag) {
//do sth
}
System.out.println("end");
}, "server").start();
Thread.sleep(1000);
flag = false;
}
}
2.防止指令重排序
乱序执行:程序里面的每行代码的执行顺序,有可能会被编译器和cpu根据某种策略,给打乱掉,目的是为了性能的提升,让指令的执行能够尽可能的并行起来。知道指令的乱序策略很重要,原因是这样我们就能够通过barrier等指令,在正确的位置告诉cpu或者是编译器,这里我可以接受乱序,那里我不能接受乱序等等。从而,能够在保证代码正确性的前提下,最大限度地发挥机器的性能。
- 对象的创建过程
源码:
Class T{
int m = 8;
}
T t = new T();
汇编码:
0 new #2 <T> //半初始化,m为默认值0
3 dup
4 invokespecial #3 <T.<init>> //调用构造方法,m = 8
7 astore_1 //将t与new的对象连接起来
8 return
- DCL单例需不需要加volite
当对象半初始化时,下面两个指令发生了指令重排,导致得到了m=0的半初始化对象
加上volite便可以解决这种指令重排的问题
-
JSR内存屏障
LoadLoad:对于这样的语句Load1;LoadLoad;Load2,在Load2及后续的读操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕;
StoreStore:对于这样的语句Store1;StoreStore;Store2,在Store2及后续的写操作执行前,保证Store1的写入操作对其他处理器可见;
LoadStore:对于这样的语句Load1;LoadStore;Store2,在Store2及后续的写入操作被刷出前,保证Load1要读取的数据被读取完毕;
StoreLoad:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续的读操作要读取的数据被访问前,保证Store1的写入操作对其他处理器可见; -
as-if-serial:不管如何重排序,单线程执行结果不会改变。
在JVM层面volatile的实现细节特别保守,保证了内存可见性,并且成功防止指令重排序。如果是对对象加入volatile,是对该对象前后加入内存屏障,具体的实现细节如下所示:
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
- volatile如何解决指令重排序
ACC_VOLATILE 字节码层级只是添加了一个标志
若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
JVM层级添加内存屏障
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。
- 系统底层如何实现数据一致性
- MESI如果能解决,就使用MESI
- 如果不能,就锁总线
- 系统底层如何保证有序性
- 内存屏障sfence mfence lfence等系统原语
- 锁总线
volite的提高效率的应用
按块读取
程序局部性原理,可以提高效率
充分发挥总线CPU针脚等一次性读取更多数据的能力
- 一个利用按块读取提高效率的小程序
public class T02_CacheLinePadding {
private static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7;
}
private static class T extends Padding {
public volatile long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[0].x = i;
}
});
Thread t2 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start)/100_0000);
}
}
p1,p2...p7和x组成64位的内存行,防止arr[0].x和arr[1].x在同一内存行,造成缓存内容的无用刷新。
Comments | 0 条评论