并发编程-Volatile解决JMM的可见性问题

上一篇 << 下一篇 >>>Volatile的伪共享和重排序


Volatile的特性

  • a、保证变量对所有线程的可见性
  • b、禁止指令重排序优化------相当于一个内存屏障,不让顺序优化
    (指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)
    他不能够保证共享变量的原子性问题

CPU多核硬件架构剖析

CPU每次从主内存读取数据比较慢,而现代的CPU通常涉及多级缓存,CPU读主内存。
按照空间局部性原则加载局部快到缓存中,缓存速度会快很多。

并发编程-Volatile解决JMM的可见性问题_第1张图片

产生可见性的原因

因为我们CPU读取主内存共享变量的数据时候,效率是非常低,所以对每个CPU设置对应的高速缓存 L1、L2、L3 缓存我们共享变量主内存中的副本。
相当于每个CPU对应共享变量的副本,副本与副本之间可能会存在一个数据不一致性的问题。
比如线程线程B修改的某个副本值,线程A的副本可能不可见,最终导致了可见性问题。

并发编程-Volatile解决JMM的可见性问题_第2张图片

Volatile的代码展示

添加的dll文件
X:\java8\jdk\jre\bin\server 放入 hsdis-amd64.dll

/**
 * 在启动的VM Options里加入:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*JaryeThread.*
 * 即可查看volatile关键字使用的锁信息:
 0x000000010cf75c13: lock addl $0x0,(%rsp)     ;*putstatic flag
 ; - com.jarye.JaryeThread::@1 (line 18)
 *
 *
 0x000000010cf751cf: lock addl $0x0,(%rsp)     ;*putstatic flag
 ; - com.jarye.JaryeThread::main@21 (line 35)
 **/
public class JaryeThread extends Thread {
    // 1.能够保证可见  当一个线程在修改我们主内存共享变量的数据 对另外一个线程可见
    /**
     * 汇编指令lock锁的机制 能够把让工作内存主动刷新主内存数据
     */
    private volatile   static boolean flag = true;
//
    @Override
    public void run() {
        while (flag) {
        }
    }

    public static void main(String[] args) {
        // 默认的情况下创建的线程为用户线程
        new JaryeThread().start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {

        }
        // 主线程
        flag = false;
        System.out.println("主线程已经停止啦~~...");
    }
}

Volatile的底层原理

通过汇编lock前缀指令触发底层锁的机制,帮助我们解决多个不同cpu之间三级缓存之间数据同步
锁的机制两种:总线锁/MESI缓存一致性协议

总线锁【维护解决cpu高速缓存副本数据之间一致性问题】

当一个cpu(线程)访问到我们主内存中的数据时候,往总线发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。
该模式存在非常大的缺陷,就是将并行的程序变为串行,没有真正发挥出cpu多核的好处。

MESI缓存一致性协议

  • M修改 (Modified) 这行数据有效,数据被修改了,和主内存中的数据不一致,数据只存在于本Cache中。
  • E 独享、互斥 (Exclusive)这行数据有效,数据和主内存中的数据一致,数据只存在于本Cache中。
  • S 共享 (Shared)这行数据有效,数据和主内存中的数据一致,数据存在于很多Cache中。
  • I 无效 (Invalid) 这行数据无效。
    并发编程-Volatile解决JMM的可见性问题_第3张图片

A、主内存flag=true,thread-main和thread-A两个线程都为S
B、主内存设置flag=false时,thread-main设置为M
C、主内存在做写操作时,thread-main设置为E,写成功后变为S,然后总线嗅探机制将thread-A变为I
D、Thread-A从主内存中主动拉取flag的值到工作内存中,成功后设置值为S。

Volatile为什么不能保证原子性


/**
 * volatile为什么不能保证原子性:
 * 线程1、线程2同时执行count++的操作,本地内存都做了变更,
 * 由于使用MESI协议,假设线程1的执行到主内存中时,总线嗅探器告诉线程2主内存已更改,直接将线程2的数据改为失效,从主内存中读取,导致了线程2的首次count++失效
 * 然后线程2再执行count++的操作,刷新到主内存中
 * 结论:线程2的首次count++失效,导致总体count的数据出现了偏差
 *
 **/
public class VolatileAtomThread {
    // 共享的全局变量
    private  volatile static int count = 0;

    /**
     * 一定会小于10000
     */
    public static void create() {
        count++;
    }

    public static void main(String[] args) {
        ArrayList threads = new ArrayList<>();
        // 10次 10个线程 每个线程执行1000次
        for (int i = 0; i < 10; i++) {
            Thread tempThread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    create();
                }
            });
            threads.add(tempThread);
            tempThread.start();
        }
        threads.forEach((t)-> {
            try {
                // 主线释放锁同时放弃cpu执行前 等待前面10个线程执行完毕
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 100001 10002
        System.out.println(count);
    }
}


相关文章链接:
<<<多线程基础
<<<线程安全与解决方案
<<<锁的深入化
<<<锁的优化
<< << << << << << << << << << <<<线程池
<<<并发队列
<< << << << <<<如何优化多线程总结

你可能感兴趣的:(并发编程-Volatile解决JMM的可见性问题)