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的半初始化对象
批注 20200602 011625.jpg

加上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 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。

另外还提供了有序的指令无法越过这个内存屏障的作用。

  • 系统底层如何实现数据一致性
  1. MESI如果能解决,就使用MESI
  2. 如果不能,就锁总线
  • 系统底层如何保证有序性
  1. 内存屏障sfence mfence lfence等系统原语
  2. 锁总线

volite的提高效率的应用

批注 20200602 092234.jpg

批注 20200602 092302.jpg

批注 20200602 092358.jpg

批注 20200602 092427.jpg

按块读取
程序局部性原理,可以提高效率
充分发挥总线CPU针脚等一次性读取更多数据的能力

批注 20200602 094145.jpg

  • 一个利用按块读取提高效率的小程序
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在同一内存行,造成缓存内容的无用刷新。


hhhhh