Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
经典问题:
20 个线程,每个线程都对同一个 int 做自增操作 100 次,最后的结果 i 一定小余 2000。
Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。num++解析为num=num+1,明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题。
原因:
普通变量的值在线程间传递都需要通过主内存来完成。例如,线程 A 修改了一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。
原子性
是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。
Java中的原子操作包括:
- 除long和double之外的基本类型的赋值操作(long和double占用的字节数都是8,在32位操作系统上对64位的数据的读写要分两步完成,虽然 volatile 只保证可见性,但 java 内存模型保证声明为 volatile 的long和double变量的get和set操作是原子的。)
- 所有引用reference的赋值操作
- java.concurrent.Atomic.* 包中所有类的一切操作。
可见性
一个线程对一个变量进行更改操作 其他线程获取会获得最新的值
sleep 方法并没有加锁,为什么能够保证可见性。sleep是阻塞线程并不释放锁,让出cpu调度。让出cpu调度后下次执行会刷新工作内存
指令重排
在单线程中不影响最终结果,jvm 会对指令做优化排序
1 | //在线程A中执行: |
1,2 的顺序可能因为指令重排做了调换。先执行2,再执行1。
因此在多线程中可能导致线程 B 中, context 还没有初始化,但 contextReady 已经是 true,就报空指针。
内存屏障
1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
volatile
内存屏障策略:
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
因此 volatile 可以做到:
线程的可见性
volatile 变量在各个线程的工作内存中不存在一致性问题。因为每次赋值后,都会通过内存屏障更新主存中的值。
禁止指令重排
因为每次读写前后都会插入内存屏障,而内存屏障前后的指令不可被重排。
但是需要注意的是 volatile 只保证可见性,并不保证原子性。为了实现上述 i++ 的原子性运算,需要用到实现线程安全的两个保障手段:阻塞同步和非阻塞同步都是。
阻塞同步
即加锁。但是会带来线程阻塞和唤醒的性能开销
非阻塞同步
对于阻塞同步而言主要解决了阻塞同步中线程阻塞和唤醒带来的性能问题。在并发环境下,某个线程对共享变量先进行操作,如果没有其他线程争用共享数据那操作就成功;如果存在数据的争用冲突,那就才去补偿措施,比如不断的重试机制,直到成功为止,因为这种乐观的并发策略不需要把线程挂起,也就把这种同步操作称为非阻塞同步(操作和冲突检测具备原子性)。Java 中实现 CAS 乐观锁的方式是使用 AtomicInteger。
CAS(Compare-And-Swap 比较并交换)
CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
Java 的 CAS 会使用 cpu 上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作。但 CAS 也存在一些问题:
ABA 问题
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
循环时间长开销大
只能保证一个共享变量的原子操作
AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
AtomicInteger
依赖于 Unsafe 提供的一些底层能力,实现了 CAS 的乐观锁。如果
1 | // AtomicInteger |