JMM三大特性
原子性
汇编指令 --原子比较和交换在底层的支持 cmp-chxg
总线加锁机制 Synchronized Lock锁机制
public class VolatileAtomicSample { private static volatile int counter = 0; // volatile无法保证原子性 public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(()->{ for (int j = 0; j < 1000; j++) { counter++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效 //1 load counter 到工作内存 //2 add counter 执行自加 //其他的代码段? } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }
可见性 volatile -- LOCK缓存行
public class VolatileVisibilitySample { private volatile boolean initFlag = false; static Object object = new Object(); public void refresh(){ this.initFlag = true; //普通写操作,(volatile写) String threadname = Thread.currentThread().getName(); System.out.println("线程:"+threadname+":修改共享变量initFlag"); } public void load(){ String threadname = Thread.currentThread().getName(); int i = 0; while (!initFlag){ /*synchronized (object){ //i++; }*/ i++; } System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i); } public static void main(String[] args){ VolatileVisibilitySample sample = new VolatileVisibilitySample(); Thread threadA = new Thread(()->{ sample.refresh(); },"threadA"); Thread threadB = new Thread(()->{ sample.load(); },"threadB"); threadB.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }
有序性
-- 指令重排 ---> 内存屏障
查看汇编指令:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)。 即在单线程情况下,不能改变程序运行的结果
程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
double p = 3.14; //1 double r = 1.0; //2 double area = p * r * r; //3
上面例子中1,2存在指令重排操作,但是1,2不能和第三步存在指令重排操作,否则将改变程序运行的结果。
happen-before原则
1、 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)
3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,
而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4. 线程启动原则 线程的start()方法先于他的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行了start方法之时,线程A对共享变量的修改对线程B可见。
5. 传递性 A先于B,B先于C,那么A必然先于C
6. 线程终止原则 线程的所有操作先于线程的终结。Thread.join()方法的作用就是等待当前执行的线程的终止。
假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7. 线程中断规则
对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
8、对象终结规则 对象的构造函数执行,结束先于finalize()方法
指令重排code示例
public class VolatileReOrderSample { private static int x = 0, y = 0; private static int a = 0, b =0; static Object object = new Object(); public static void main(String[] args) throws InterruptedException { int i = 0; for (;;){ i++; x = 0; y = 0; a = 0; b = 0; Thread t1 = new Thread(new Runnable() { public void run() { //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间. shortWait(10000); a = 1; //是读还是写?store,volatile写 //storeload ,读写屏障,不允许volatile写与第二部volatile读发生重排 //手动加内存屏障 UnsafeInstance.reflectGetUnsafe().storeFence(); x = b; // 读还是写?读写都有,先读volatile,写普通变量 //分两步进行,第一步先volatile读,第二步再普通写 } }, "t1"); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; UnsafeInstance.reflectGetUnsafe().storeFence(); y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); /** * cpu或者jit对我们的代码进行了指令重排? * 1,1 * 0,1 * 1,0 * 0,0 */ String result = "第" + i + "次 (" + x + "," + y + ")"; if(x == 0 && y == 0) { System.err.println(result); break; } else { System.out.println(result); } } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); } }
内存屏障code示例
1.写写storestore 2.写读storeload 3.读写loadstore 4.读读loadload
public class VolatileBarrierExample { int a; volatile int m1 = 1; volatile int m2 = 2; void readAndWrite() { int i = m1; // 第一个volatile读 int j = m2; // 第二个volatile读 a = i + j; // 普通写 m1 = i + 1; // 第一个volatile写 m2 = j * 2; // 第二个 volatile写 } }