日常记录——多线程与高并发—volatile概念、功能、原理、举例、volatile与synchronized的区别

一、概念

volatile是一个特征修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

二、功能

1.保证数据可见性
2.禁止指令重排序
注意:不保证原子性
例如:修饰变量的++操作。

三.原理

1.保证数据可见性:依靠MESI(cpu的高速缓存协议)+总线锁,在java内存中,分为主内存和线程内存,主内存中记录变量的值,线程内存在获取变量的值时,先将值copy到自己的内存,执行本线程内操作后,再将变量值写回到主内存。线程之间的变量值的获取通过主内存来操作,那么问题就会产生,如果A线程获取到值修改还没回写到主内存,B从主内存获取A修改前的值,进行操作,那么就会在并发过程中不能保证变量值的准确性,发生脏读。
那MESI保证什么:当一个线程修改缓存中的字节时,服务器中其他线程会被通知,它们的缓存将视为无效。所以上面的问题就被绝解决了,当A线程修改了变量值,回通知B线程当前获取值无效,所以B会重新获取A线程回写到主内存的值。
MESI:CPU caceh (CPU 缓存) 中的 每个 caceh line (cpu 缓存中的单元)使用标值记录缓存状态

状态 含义 监听任务
M 修改 (Modified) 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E 独享(Exclusive) 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared) 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)
I 无效 (Invalid) 该Cache line无效。

caceh line相关扩展:caceh line 一般读取64字节内存,如果两个long变量x,y相邻,则会被读取到一个caceh line中,当A,B两个线程分别占有了同一个caceh line,当A线程修改x值,B线程修改y值,则都会讲该缓存行标志位修改为无效,这就是相互影响的伪共享问题(不会造成数据不一致,但影响效率),基于伪共享提高效率的办法就是,缓存行对齐,一个变量在他前后补齐其余字节数,牺牲空间,提高效率,代码实现为:
未对齐缓存行

public class NoTabCatchLine {
    private volatile long x = 0L;
    public static NoTabCatchLine arr[] = new NoTabCatchLine[2];
    static {
        arr[0] = new NoTabCatchLine();
        arr[1] = new NoTabCatchLine();
    }

    public static void main(String[] args) throws InterruptedException {
        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;
            }
        });
        long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime()-start)/100_0000);
    }
}

对齐缓存行:

public class TabCatchLine {
    private long p1,p2,p3,p4,p5,p6,p7;
    private volatile long x = 0L;
    private long p8,p9,p10,p11,p12,p13,p14;
    public static TabCatchLine arr[] = new TabCatchLine[2];
    static {
        arr[0] = new TabCatchLine();
        arr[1] = new TabCatchLine();
    }

    public static void main(String[] args) throws InterruptedException {
        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;
            }
        });
        long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime()-start)/100_0000);
    }
}

对齐缓存行执行时间大约为未对齐的一半。
总线锁:无法被缓存的对象或者跨越多个缓存行的数据,会使用总线锁,总线锁会锁住总线,使得其他cpu不能访问内存中其他的地址,效率低。

2.禁止指令重排:CPU为了提高执行效率,编译过后的指令在cpu会优化指令并发执行指令,但是在加上volatile关键字后,编译过后的指令在cpu会添加读屏障和写屏障,防止指令重排,并发执行安全。
JVM虚拟机规范屏障
loadload屏障:load1;loadload:load2,意为在load2读取执行前必须保证load1读取执行完。
loadstore屏障:load;loadstore:store,意为在store写入执行前必须保证load读取执行完。
storestore屏障:store1;storestore:store2,意为在store2写入执行前必须保证store1写入执行完,并对所有处理器可见。
storeload屏障:store;storeload:load,意为在load读取执行前必须保证lstore写入执行完,并对所有处理器可见。
硬件层级屏障支持
sfence:在sfence前的写操作指令必须在sfence指令后的写操作前完成。
lfence:在lfence前的读操作指令必须在lfence指令后的读操作前完成。
mfence:在mfence前的读、写操作指令必须在mfence指令后的写、读操作前完成。
实现顺序:
字节码层级:变量的 access flags 加了个volatile 修饰符引用。
虚拟机层级:loadload屏障 volatile读操作 loadstore屏障 ;storestore屏障 volatile写操作 storeload屏障 。
例如单例模式下创建一个对象:

class Singleton {
	private static Singleton instance;
	private Singleton(){}
	public static Singleton getInstance() {
		if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
			synchronized (Singleton.class) {
				if ( instance == null ) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

instance = new Singleton();
这并不是一条原子性语句,他会分为三个步骤
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上述的2、3步骤没有依赖的关系,3是依赖1的,经过指令重排并发执行的情况下,第1步执行过后,可能会出现先执行第3步,后执行第2步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,返回fasle,导致结果返回了一个没有初始化对象的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,保证对象的完整。

四、举例

1.`子线程一直循环判断flag变量是否为true,如果为fasle,线程执行结束。

public class volatiletest {
    public static /*volatile*/ boolean flag = true;

    public static void main(String[] args) {
        new Thread(() ->{
            System.out.println("线程开始");
            while (flag){

            }
            System.out.println("线程结束");
        }).start();
        try {
            Thread.sleep(1000);
            flag = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

但因为没有加volatile,主线程和子线程获取变量值不同,导致子线程一直运行中。
日常记录——多线程与高并发—volatile概念、功能、原理、举例、volatile与synchronized的区别_第1张图片
将volatile开启后,主线程之下子线程缓存一致,子线程在主线程睡1秒后结束执行。
日常记录——多线程与高并发—volatile概念、功能、原理、举例、volatile与synchronized的区别_第2张图片

五、volatile与synchronized的区别

1.Volatile只能修饰变量,synchronized只能修饰方法和语句块
2.保证可见性原理不同,volatile通过MESI+总线锁,synchronized使用对象的监视器
3.synchronized可以保证原子性,volatile不能保证原子性
4.synchronized引起阻塞,volatile不会引起阻塞

你可能感兴趣的:(多线程与高并发)