现代计算机的内存模型
1.指令与数据
- 指令:由CPU处理器执行
- 数据:存储在主内存当中
2.问题情景
问题1:
计算机的存储设备与处理器的运算速度有几个数量级的差距
解决1:
现代计算机系统加入一层读写速度接近处理器运算速度的高速缓存(Cache),作为内存与处理器之间的缓冲(仅次于CPU寄存器),避免了每次从主存读取数据
3.高速缓存
- 一级缓存L1,二级缓存L2,三级缓存L3(多核共享)
- 内存模型
4.读写操作
5.总线
缓存一致性问题
1.问题描述
- 多个处理器运算任务都涉及同一主内存区域时,可能导致缓存数据不一致
2.解决
- 总线+锁
在总线上加上锁,被占用时其他CPU就不能操作,效率较低
- 窥探技术+缓存一致性协议
1.窥探技术:保证CPU知道数据变化
所有数据传输都发生在一条共享的总线上,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期
当处理器发现缓存行对应的内存地址被修改,就会将当前缓存行设置无效状态
当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中
2.缓存一致性协议:数据不同时怎么处理
各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作
协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等
以 MESI协议(支持回写高速缓存的协议)为例
核心:当CPU写数据时,如果发现操作的变量是共享变量(其他CPU中也存在该变量的副本),会发出信号通知其他CPU将该变量的缓存行置为无效状态,当其他CPU需要读取这个变量时,发现缓存该变量的缓存行是无效的,那么它就会从内存重新读取
- MESI协议下, CPU中每个缓存行标记的4种状态
指令重排序问题
1.重排序层面
- 编译器优化的重排序
- 指令级并行的重排序,由处理器完成
如果不存在数据依赖性,那么处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序
2.重排序原因
- 书写代码和CPU中执行顺序不一致,为了提升CPU的执行效率
- 可能导致多处理器运行同一段代码的结果不一致(单线程保证一致)
3.解决多处理器执行同一段代码结果不一致
- 内存屏障:硬件层的技术
种类: Load Barrier 和 Store Barrier即读屏障和写屏障.
作用: 1.阻止屏障两侧指令重排序
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效
- Java是跨平台的,不同平台内存屏障不一致
4.解决不同的硬件提供的内存屏障不一致
- java屏蔽掉这些差异,通过jvm生成内存屏障的指令
JMM(Java内存模型)
- Java内存模型类比于计算机内存模型
1.作用
- 屏蔽各种硬件和操作系统的内存访问差异,让Java程序在各种平台都能达到一致的内存访问效果
2.说明
- 为了更好的执行性能,JMM 没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存打交道,也没有限制编译器优化重排序,所以Java内存模型会存在缓存一致性问题和指令重排序问题
- JMM规定所有的变量(实例变量和静态变量, 不包括局部变量)都存在主内存中(相当于计算机的物理内存), 每个线程(线程跑在jvm中,而jvm可以理解成是一个虚拟的系统)都有自己的工作内存(类似于计算机模型的高速缓存),线程的工作内存保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存,并且每个线程不能访问其他线程的工作内存
并发编程三大特性
1.(操作)原子性
- 小于32位的基本类型的操作都具有原子性
对于long和double变量,将其作为2个原子性的32位值来对待,而不是一个原子性的64位值, 将一个long型的值保存到内存的时候,可能是2次32位的写操作
2个竞争线程想写不同的值到内存的时候,可能导致内存中的值是不正确的结果
- 案例
1.i=666 原子性
直接将数值666写入到工作内存中
2.i=j 非原子性
实际上涉及两个操作:先去读j的值,再把j的值写入工作内存
两个操作分开都满足原子性
3.i=i+1 非原子性
4.i++ 非原子性
2.(结果)可见性
- 当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
- JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值
依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此
- 区分
volatile:保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以volatile保证多线程操作变量的可见性,不保证原子性
synchronized和Lock也能保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,保证原子性
final也可以实现可见性,直接就不能修改
3.(指令)有序性
- JMM定义
如果在本线程内观察,所有的操作都是有序的(as-if-serial)
意思是as-if-serial的语义,即不管怎么重排序(编译器和处理器为了提高并行度)程序的执行结果不会被改变
如果在一个线程中,观察另一个线程,所有的操作都是无序的
意思是允许编译器和处理器对指令进行重排序, 会影响到多线程并发执行的正确性
volatile与synchronized既保证可见性,又保证有序性
JMM保证有序性原则(不加任何关键字)
1.happens-before原则(先行开发原则)
- 定于与JSR 133规范中
- 程序次序规则
书写在前面的操作先发生于书写在后面的操作
- 管程锁定规则
解锁操作先发生于加锁操作
- volatile变量规则
对volatile变量的写操作先发生于对这个变量的读操作
- 线程启动规则
Thread对象的start()方法先发生于此线程的每个动作
- 线程终止规则
线程中所有的操作都先发生于线程的终止检测
- 线程中断规则
对线程interrupt()方法的调用先发生于检测中断线程代码
- 对象终结规则
对象的初始化完成先发生于他的finalize()方法的开始
- 传递性规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
2.案例分析
volatile bool flag = false;
int b = 0;
public void read() {
b = 1; //1
flag = true; //2
}
public void add() {
if (flag) { //3
int sum =b+b; //4
System.out.println("bb sum is"+sum);
}
}
按 happens-before原则分析:
1.flag加上volatile关键字,那就禁止了指令重排,也就是1 happens-before 2了
2.根据volatile变量规则(volatile变量的写先于读),2 happens-before 3
3.由程序次序规则,得出 3 happens-before 4
4.由传递性,1 -> 2 -> 3 -> 4,得出1 happens-before 4
5.因此妥妥的输出sum=2
面试常问—volatile
1.volatile概述
- JAVA虚拟机提供的最轻量级同步机制
- 修饰变量,但不能是局部变量
- 保证共享变量在多线程间的可见性(数据一致性)
新值被立即同步回主内存
使用前立即从主内存刷新
- 保证有序性
禁止指令重排序(内存屏障)
- 不保证原子性与线程安全
- 保证可见性、有序性,但不保证原子性
2.volatile底层保证可见性
![在这里插入图片描述](https://img-blog.csdnimg.cn/cb232357c8034865a17e58912c645367.png?x-oss-process=image/watermark,type_d3 F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiA5Liq5q2j5Zyo5Yqq5Yqb55qE6I-c6bih,size_18,color_FFFFFF,t_70,g_se,x_16)
3.volatile底层保证内存屏障
- 实际上volatile保证可见性和禁止指令重排都跟内存屏障有关
- 双重检查锁的单例模式
public class Singleton {
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
观察有volatile关键字和没有volatile关键字时的instance所生成的汇编代码发现
有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令
lock指令相当于一个内存屏障,它保证以下这几点:
1.重排序时,不将后面的指令重排序到内存屏障前
2.将本处理器的缓存写入内存
3.若是写入动作,会导致其他处理器中对应的缓存无效
小结:第2、3点不就是volatile保证可见性的体现嘛,第1点就是禁止指令重排列的体现
- volatile底层如何保证内存屏障
- volatile内存语义
为了实现volatile的内存语义,Java内存模型采取以下的保守策略
1. 在每个volatile写操作的前面插入一个StoreStore屏障
2. 在每个volatile写操作的后面插入一个StoreLoad屏障
3. 在每个volatile读操作的前面插入一个LoadLoad屏障
4. 在每个volatile读操作的后面插入一个LoadStore屏障
内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦
同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性
4.举个栗子
int i=0; i=i+1;
以上在单线程和多线程环境下运行情况
- 单线程
- 多线程
5.volatile与原子性
- volatile不保证原子性
- 如何保证原子性
1.粗粒度:synchronized(重量级)或lock(轻量级)
2.细力度:CompareAndSet轻量级无锁编程
6.volatile与synchronized区别
- volatile修饰变量;synchronized修饰代码块或方法
- volatile保证可见性,有序性,但不保证原子性;synchronized都保证
- volatile不会造成线程阻塞,synchronized会
面试常问—synchronized
1.概述
- 独占式悲观锁
强制锁:悲观的认为一定会有线程争抢的问题
- 可重入锁
对次数多的线程的优化策略
同一个线程可以多次获取同一把锁,不用切上下文
- 重量级锁
信息量大
- 互斥锁
一次只允许一个
- 作用范围
1.对象锁:成员变量和非静态方法,对象的实例
synchronized(obj){}
public synchronized void add(){}
2.类锁:静态方法,锁住Class实例
synchronized(Person.class){}:取得Person类的反射对象的对象锁
2.锁膨胀
-
锁有无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态4种状态
-
锁可以升级,但不能降级 -> 锁膨胀
-
锁膨胀过程
3.内部结构
- JVM中类的存储结构
- MarkWord的结构
- JVM中类的存储结构
ObjectMonitor() {
_header = NULL;
_count = 0;//记录该线程获取锁的次数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;//哪个线程持有锁
_WaitSet = NULL;//处于等待状态的线程
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程(被阻塞的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- 原理图
- 解析
当多个线程同时访问一段同步代码时,首先会进入_EntryList(阻塞)队列中
当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程
同时monitor中的计数器_count加1
若线程调用wait()方法,将释放当前持有的monitor
_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒
若当前线程执行完毕也将释放monitor(锁)并复位变量的值
以便其他线程进入获取monitor(锁)
面试常问—ThreadLocal类
1.需求场景
- 对同一个Runnable的执行,能根据线程的不同而有所区分
2.案例
public class Test1_original{
public static void main(String[] args) {
final int arg=0;
Thread t1=new Thread(new Runnable() {
public void run() {
task1( arg );
}
});
t1.start();
}
public static void task1(int arg) {
task2( arg);
//如果之后的方法里使用到参数,那么需要继续传递,如不传递,则参数丢失,如有多个参数,则每个参数都要传递,造成代码冗余
}
public static void task2(int arg) {
System.out.println(arg)
}
}
3.解决方案
public class Test2 {
//arg相当于Map<ThredId,Integer> --> 空间大小不可控
static ThreadLocal<Integer> arg = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
//初始化参数
arg.set(0);//arg.put(Thread.currentThread.getId(),0);
//参数无需再次传递
task1();
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
public void run() {
//初始化参数
arg.set(1);//arg.put(Thread.currentThread.getId(),1);
//参数无需再次传递
task1();
}
});
t2.start();
}
public static void task1() {
task2();
}
public static void task2() {
//arg.get(Thread.currentThread.getId()
System.out.println(arg.get());
}
}
4.分析
- 只要能访问到ThreadLocal变量的地方,都可以获取到指定的值
- 它与static的区别?
ThreadLocal#get获取到的值,对每个线程是唯一的.
5.ThreadLocal源码分析
- set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- createMap方法
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- get方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}
- ThreadLocalMap
//Entry为ThreadLocalMap静态内部类,对ThreadLocal的引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
---------------------------------------------------------
//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//通过hashCode与length位运算确定出索引值i(存储在table数组中的位置)
//这样每个线程都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
---------------------------------------------------------
//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
//一个Thread只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象
//为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table
//那么问题来了,ABC在table中的位置是如何确定的?
//为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i, 这就是set方法的功能.
//ThreadLocalMap中set方法。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);
//遍历tab,如果已经存在则更新值
for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
---------------------------------------------------------
//key.threadLocalHashCode是如何实现的
//ThreadLocal中threadLocalHashCode相关代码.
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
//自增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//static的原因,在每次new ThreadLocal时会使threadLocalHashCode值自增一次,增量为0x61c88647
//0x61c88647是斐波那契散列乘数,优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突
---------------------------------------------------------
小结:1.对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的
2.对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的
---------------------------------------------------------
//ThreadLocal中get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
---------------------------------------------------------
//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
6.synchronized与ThreadLocal的区别
相同:都为了解决多线程中相同变量的访问冲突问题
不同:1.synchronized是通过线程等待,牺牲时间来解决访问冲突
2.ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值
应用场景:当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal