吊打面试官之直面恐惧----JMM(Java内存模型)
谈到JMM,第一眼感觉还是很陌生,依稀地记得是一个java抽象的内存模型,是为了解决Java不同操作系统中或硬件环境中多线程并发的问题。
这是一个抽象的概念,更像是一个规定和约束,屏蔽了底层操作系统和硬件的内存访问差异。规定了一个线程执行写入操作后,何时以及以何种方式对另一个线程可见。
感觉和AQS差不多,都是偏底层的,抽象出来的框架一样的东西,因此每次看完后,一关闭,转眼就忘地一干二净了,这能忍吗,我还怎么吊打面试官(╯▔皿▔)╯
故事起源
要解决这个问题,故事还得从CPU说起。
CPU为了性能,构建了三级缓存,比直接操作内存的速度快多了。问题是如何解决缓存和主存数据一致性的问题。线程A在CPU缓存中更新了数据,还没来得及刷回主存,线程B读主存读到了数据,这时就是脏数据了。这是可见性问题。
CPU和编译器为了性能优化,运行指令重排序,只要保证最终运行结果一致就行了。这在单线程环境下是没问题的,但是多线程环境下就会出现结果不一致的问题。**这是有序性的问题 **。
在程序中,你执行了i++操作,看似是一个原子指令,但是在底层涉及读-改-写三步操作,如果其它线程插手进来,那执行结果不就又乱套了吗。这是原子性的问题。
JMM
为了解决这些问题,就有了JMM , 定义了一系列规范规避操作系统和硬件的内存访问差异,解决Java程序的并发问题。
具体来说,JMM抽象了一个工作内存,每个线程独有,每次操作时都先把共享变量复制到操作工作内存,然后在修改,修改完后刷回主存。 拿i++举例就是线程先把变量i 读进工作内存,然后执行i++操作,操作完后再刷回主存。
那怎么解决可见性的问题呢,JMM定义了happens-before原则规定了操作的发生顺序。定义了规则就要有执行者,这时候干活的就是volatile关键字。
具体实现
我们在幼儿园就学过用volatile关键字修饰的共享变量能保证可见性和有序性。
当写变量的时候JMM会把修改立即刷回主存,
读变量的时候会把当前工作内存中的值作废,直接去主存中读取最新值。
为什么volatile关键字能做到?
底层通过内存屏障实现,相当于一个栅栏,告诉CPU和编译器,这个位置前后的某些操作不能乱动
这里涉及四种内存屏障

丸啦,看到这里感觉天塌了,叽里呱啦说什么呢?这是人能看东西吗,我终究还是没能吊打面试官(T_T)。
先别走,这个表格就是我用AI生成的,下面我再一一解释。
当读一个volatile变量时

当读一个volatile变量时,在后面插入LoadLoad屏障,表示后面的读操作不能排在volatile读前面。
还要插入一个LoadStore屏障,表示后面写操作不能排在volatile读操作之前。
当写一个volatile变量时

在对volatile修饰的变量执行写操作之前,插入StoreStore屏障,表示普通写操作不能排在volatile写操作之后。
在对volatile修饰的变量执行写操作之后,插入StoreLoad屏障,这个时候是为了将写后的变量立即刷回主存以及避免脏数据,所以要禁止volatile写后的读写操作
所以简单记就是:写 volatile 变量前加 StoreStore,后加 StoreLoad;读之后加 LoadLoad 和 LoadStore。
要注意的就是volatile关键字是不保证原子性的,上面的所有操作都是分分步骤完成的。
那i++举例就能很好的理解。
扩展
那我要怎么保证原子性呢?那就得加锁,sychronized 和 reentrantLock选一把 , 如果面试官追问还能再聊下去。
那对于 i++ 这种统计操作, 还能用原子类AtomicInteger / AtomicLong 之类的保证线程安全。
底层用的CAS+自旋的操作实现, 又能再唠唠CAS及其底层实现。
嫌AtomicInteger性能不够?还有没有更好的?有的兄弟有的。
LongAdder不就来了吗,实现的思想就是分散热点 。
它是Striped64的子类。主要有long类型的 base 和 cell数组。
▼plain复制代码static final int NCPU = Runtime.getRuntime().availableProcessors(); /** * Table of cells. When non-null, size is a power of 2. */ transient volatile Cell[] cells; /** * Base value, used mainly when there is no contention, but also as * a fallback during table initialization races. Updated via CAS. */ transient volatile long base; /** * Spinlock (locked via CAS) used when resizing and/or creating Cells. */ transient volatile int cellsBusy;
在多线程环境下,longAdder有多个CAS计算窗口,将值分散到cells数组中,不同的线程会分散到数组的不同的槽位,各个线程只会到自己的槽中做CAS操作,如果要取到总数,就可以统计所有的槽数。
用空间换时间。
那么说到LondAdder你又能联想到什么?ConcurrentHashMap(JDK1.8)里面是不是也有类似的设计?
当获取集合size()大小时,底层参考这个思想。
平日里维护map里的节点数量,会通过CAS来修改baseCount,成功就返回,失败有线程竞争,就通过hash选择一个countercell对象进行修改,最终结果是baseCount + countercell
put和remove都会更改countercell。
这一层层套下来,各种体系一环套一环,吊打面试官指日可待o( ̄▽ ̄)ブ
