4. JMM:java内存模型
主要关注多线程间的可见性问题与多条指令执行时的有序性问题
4.1 Java内存模型
JMM即Java memory model,定义了主存(公有)、工作内存(私有)抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等
4.2 可见性
- 问题:退不出的循环:
- 初始时,t线程从主存中读取一个值到工作内存
- 如果t线程在运行时频繁读取主存中的值,JIT编译期会将这个值缓存到t线程的工作内存中的高速缓存中,减少对主存的访问,调高效率
- 这个时候改变主存中的要被读取的值,但是t线程却读不到改变的这个值,因为t线程是从高速缓存中读取的这个值
- 解决方法:对变量添加volatile关键字,这样线程就不会从高速缓存中读取这个值,只会从主存中读取
- synchronized也会保证变量具有可见性,但是volatile相比起来更轻量,因为不需要创建Monitor
- 可见性和原子性是不同的,可见性适合一个写多个读,只能保证线程看到最新值,却不能解决指令交错
4.3 有序性
-
指令重排的优化,流水线实现任务(指令级并行),提高吞吐量,分阶段分工
-
CPU层面,每条指令可分为五个阶段:
- 取指令(IF)
- 指令译码(ID)
- 执行指令(EX)
- 内存访问(MEM)
- 数据写回(WB)
-
并发压测工具jcstress
-
指令交错会影响输出结果,变量前加volatile会禁止赋值之前的代码重排序
4.4 volatile原理
保证可见性
-
写屏障保证在该屏障之前对共享变量的改动,都同步到主存中
public void actor2(I_Result r){ num = 2; ready = true;//ready 被volatile修饰 //写屏障 }
-
读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据
public void actor1(I_Result r){ //读屏障 //ready被volatile修饰 if(ready){ r.r1 = num + num; }else{ r.r1 =1 ; } }
保证有序性
代码同上
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
注意无论读写屏障都不能处理指令交错的问题
double-check locking问题
double-check locking 单例模式特点:
- 懒惰实例化
- 首次使用getInstance()才使用synchronized加锁,后续使用无需加锁
- 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外,相当于脱离了synchronized的保护
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE = null;
public static Singleton getInstance(){
if (INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
多线程环境下,上面代码有问题:
jvm优化,指令重排时可能出现先给INSTANCE赋值,再调用Singleton构造方法,这样的话就有可能在赋值之后就被其他线程插入,判断INSTANCE不为空,进行使用,但是这时构造方法还没执行,也就是说使用的是未初始化完毕的INSTANCE
解决方法:
给INSTANCE加volatile关键字,写屏障会让赋值操作前的操作(构造方法)不能被重排序到后面。
4.5 happens-before规则
规定了对共享变量的写操作对其他线程的读操作可见,主要是对主存,变量都是成员变量或静态成员变量
- 线程解锁之前,对同步代码块里的变量的写,对于接下来加同一个锁的其他线程对该变量的读可见
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其他线程得知它结束后的读可见
- 一个线程打断另一个线程前对变量的写操作,对于其他得知被打断线程被打断后对变量的读可见
- 对变量默认值的写操作,对其他线程对该变量的读可见
- 具有传递性,volatile的放指令重排,会把加了volatile关键字的变量赋值操作前的其他变量赋值也读可见
4.6 总结
可见性由jvm缓存优化引起,有序性由jvm指令重排序引起
5. 无锁并发
5.1 主要面对问题
-
不加锁实现线程安全
-
使用原子整数类关键字
AtomicInteger
5.2 CAS与volatile
-
CAS:
compareAndSet(prev,next)
方法,也可称为(Compare And Swap),是原子操作。 -
底层:lock cmpxchg指令,在多核状态下,执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕再开启总线,所以是原子的。
-
volatile在这里的作用是保证CAS每次都能读取到共享变量的最新值,也就是可见性。所以共享变量需要使用volatile修饰
-
一般情况,CAS比synchronized效率高一些,因为无锁情况下,即使重试失败,线程依旧在高速运行,不会阻塞,但是synchronized会让线程在没有获得锁的情况下,发生上下文切换,进入阻塞态。
-
需要额外CPU支持(线程数小于核心数),不然线程分不到时间片,也会发生上下文切换,进入可运行状态
-
基于乐观锁的思想,不怕别的线程修改共享变量
5.3 原子整数
-
有AtomicBoolean,AtomicLong,以AtomicInteger为例:
AtomicInteger i = new AtomicInteger(0);
-
几个函数:
i.incrementAndGet();//++i i.getAndIncrement();//i++ i.getAndAdd(5);//i+5,获取改写之前的值 i.addAndGet(5);//i+5,获取改写之后的值 i.decrementAndGet();//--i i.getAndDecrement();//i-- i.updateAndGet(value -> value * 10);//函数式接口类型,lambda表达式,底层也是CAS
5.4 原子引用
-
AtomicReference:用来保护不是基本类型的变量
AtomicReference<String> ref = new AtomicReference<>("A");
-
ABA问题,无法判断共享变量是否被别的线程修改过
-
使用AtomicStampedReference,添加版本号解决ABA问题
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);//加入参数版本号
- 每次修改共享变量时,版本号加一
- 可以得知共享变量变动了多少次
-
如果不关心修改了多少次,单纯关心是否修改过,就可以使用AtomicMarkableReference
-
AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A",true);//加入参数是否被标记
5.5 原子数组
当需要保护的不是引用而是引用的对象时,可以使用原子数组,AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。
-
几个函数式接口:
- Supplier 提供者,无中生有 ()->结果
- Function 函数,一个参数一个结果(参数)->结果,BiFunction(参数1,参数2)->结果
- consumer 消费者 一个参数没结果 (参数)->void,BiConsumer(参数1,参数2)->void
-
//测试 private static <T> void demo( Supplier<T> arraySupplier, Function<T,Integer> lengthFun, BiConsumer<T,Integer> putConsumer, Consumer<T> printConsumer ){ //... }
5.6 字段更新器
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,只能配合volatile使用
- 操作的是对象里的属性
5.7 原子累加器
-
性能更强,专门做累加,LongAdder(),无参 从0开始
-
提升的原因:有竞争时,设置多个累加单元,Thread-0累加Cell[0] (累加单元),而Thread-1累加Cell[1],最后将结果汇总,累加时操作不同的Cell变量,减少了CAS重试失败,提高性能。
-
LongAdder源码关键:
-
//累加单元数据,懒惰初始化 transient volatile Cell[] cells; //如果没有竞争,则用CAS累加这个域 transient volatile long base; //在cells创建或扩容时,置为1,表示加锁 transient volatile int cellsBusy;
-
CAS锁,通过cas操作加锁
-
CPU和内存速度差异大,预读数据至缓存来提升效率。缓存以缓存行为单位,缓存行:对应一块内存,一般是64byte(8个long),但是会产生数据副本,同一份数据会缓存到不同核心的缓存行中,CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个缓存行必须失效
-
一个Cell为24个字节(16个字节的对象头和8字节的value),一个缓存行可以存两个cell,如果两个不同的核心修改缓存行中的一个cell,都会让对方的缓存行失效。还得去内存中读取
-
使用Contented解决,原理是使用此注解的对象或者字段的前后各增加128字节大小的padding,让CPU将对象预读至缓存行时占用不同的缓存行,不会造成让对方的缓存行失效。
-
add源码:longAccumulate三个部分:创建cells,cell未创建,cell已创建
- cells已经创建好了,但是cell没创建
-
最终结果通过sum方法统计
public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
-
5.8 Unsafe
- Unsafe对象提供了底层的操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
}
static Unsafe getUnsafe() {
return unsafe;
}
-
theUnsafe单例,私有
-
CAS操作:
Unsafe unsafe = UnsafeAccessor.getUnsafe(); Field id = Student.class.getDeclaredField("id"); Field name = Student.class.getDeclaredField("name"); // 获得成员变量的偏移量 long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id); long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name); System.out.println(student); Student student = new Student(); // 使用 cas 方法替换成员变量的值 UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
-
实现原子整数类
class AtomicData { private volatile int data; static final Unsafe unsafe; static final long DATA_OFFSET; static { unsafe = UnsafeAccessor.getUnsafe(); try { // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性 DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data")); } catch (NoSuchFieldException e) { throw new Error(e); } } public AtomicData(int data) { this.data = data; } } public void decrease(int amount) { int oldValue; while(true) { // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解 oldValue = data; // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) { return; } } } public int getData() { return data;
5.9 总结
主要学习CAS与volatile,以及一些API,需要知道高性能原子累加器LongAdder的源码,以及伪共享的原理