5. 共享模型之内存
5.1 Java 内存模型
Java内存模型(Java Memory Model,JMM)是一种规范,定义了Java程序中多线程并发访问共享内存时的行为规则。它规定了线程之间如何通过内存进行通信,以及如何保证程序的正确性和一致性。
-
内存模型:
-
Java内存模型描述了Java虚拟机如何处理内存,以及线程如何与内存交互。它定义了线程之间的共享内存模型,而不同的虚拟机实现需要按照该模型来实现内存的管理和线程的同步。
-
-
主内存和工作内存:
-
Java内存模型中有两种类型的内存:主内存(Main Memory)和工作内存(Working Memory)。
-
主内存是所有线程共享的内存区域,存储着对象实例、静态变量等数据。
-
每个线程都有自己的工作内存,存储着主内存中的部分数据的副本,用于线程的读写操作。
-
-
内存屏障:
-
Java内存模型使用内存屏障(Memory Barrier)来保证内存可见性和操作顺序的一致性。
-
内存屏障是一种指令,用于强制线程在某些位置同步主内存和工作内存中的数据,以确保线程之间的数据可见性和一致性。
-
在Java中,volatile变量的读写操作会在其前后插入内存屏障,保证了volatile变量的可见性。
-
-
Happens-Before关系:
-
Java内存模型定义了Happens-Before关系,用于描述操作之间的顺序关系。
-
如果一个操作A Happens-Before另一个操作B,那么在多线程环境中,操作A的结果对于操作B是可见的,并且操作A的执行顺序在操作B之前。
-
-
同步操作:
-
Java内存模型提供了一系列同步操作来实现多线程之间的同步和协作,包括synchronized关键字、volatile变量、Lock接口等。
-
这些同步操作可以保证线程之间的数据可见性、操作的原子性和有序性。
-
可见性问题:
-
如果 t 线程程频繁从主内存中读取 run 的值,JIT 编译器会将主存中run的值缓存至自己工作内存中的高速缓存中,减少对主存的访问,提高效率。这就导致了main 线程修改了 run 的值,并同步至主存后,t 从自己工作内存中的高速缓存中读取这个变量的值仍然是旧值。
-
如果一个线程在本地内存中修改了共享变量的值,其他线程可能无法立即看到这个修改后的值,因为修改可能还没有被刷新到主内存。
volatile变量:
-
volatile关键字可以用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存,保证了可见性。
-
对一个volatile变量的写操作会立即被其他线程看到,即使是在不同的CPU缓存中。
-
volatile 修饰的变量,可以禁用指令重排,保证有序性。
6. 共享模型之无锁
6.1 乐观锁
乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。
-
乐观锁多用于“读多写少“的环境,用来避免频繁加锁影响性能;
-
悲观锁多用于”写多读少“的环境,用来避免频繁失败和重试影响性能。
6.2 CAS与volatile
CAS(Compare and Swap / Set)是一种基本的原子性操作,用于实现多线程环境下的无锁算法。它是一种乐观锁算法,通过比较并交换的方式来更新共享变量的值。
基本原理:
-
CAS操作包括三个参数:共享变量的内存地址V、旧的预期值A和新的值B。
-
如果当前共享变量的值等于预期值A,那么就将该变量的值更新为新值B;否则不做任何操作。
-
CAS操作是原子性的,即在执行CAS操作时不会被其他线程中断,因此可以保证操作的一致性。
CAS的缺点:
-
ABA问题:如果在CAS操作执行之前,共享变量的值被改变了多次,且最终又恢复为原始值,那么CAS操作可能会误以为共享变量的值没有发生变化,导致数据的不一致。
-
自旋消耗:如果CAS操作失败,线程会一直自旋重试,直到成功为止。这会导致CPU资源的浪费,特别是在高并发的情况下。
Java中的CAS:
-
Java中的CAS操作通过Unsafe类的相关方法来实现,例如
compareAndSwapInt()
、compareAndSwapLong()
和compareAndSwapObject()
等。
6.3 原子操作类
-
Java提供了
java.util.concurrent.atomic
包,其中包含了一系列原子操作类,它们底层使用了CAS算法来实现线程安全的操作。 -
原子整数类有AtomicBoolean、AtomicInteger、AtomicLong
-
原子引用类有AtomicReference、AtomicMarkableReference、AtomicStampedReference
ABA 问题及解决
ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A,这个时候使用 CAS 是检查不出变化
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类AtomicStampedReference
类来解决 ABA 问题。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference
。
boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)
:比较并设置操作,如果当前引用值和标记位与预期值相等,则更新为新的引用值和标记位,并返回true;否则不做任何操作并返回false。
原子数组
-
原子数组类有AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
-
不安全的数组:
demo(
()->new int[10],
(array)->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
);
-
安全的数组:
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
字段更新器
-
AtomicReferenceFieldUpdater // 域 字段
-
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
-
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
java.lang.IllegalArgumentException: Must be volatile type
原子累加器
jdk8以后添加了专门做累加的类LongAdder
,与传统的 AtomicLong
相比,LongAdder
在高并发情况下性能更好,因为它将累加的值分散存储在多个单元中,减少了竞争,从而提高了并发性能。
-
LongAdder
内部维护了一个数组(Cell[]
),数组的大小是固定的,并且每个数组元素都是一个独立的long
值,用于记录部分累加值。Cell 是 java.util.concurrent.atomic 下 Striped64 的一个内部类。 -
当多个线程同时对
LongAdder
进行累加操作时,LongAdder
会将累加操作分散到数组中的不同元素上,当需要获取累加结果时,LongAdder
会将数组中的所有元素值相加得到最终的累加结果。 -
内部有一个base变量,一个Cell[]数组。
-
base
变量:非竞态条件下,直接累加到该变量上 -
Cell[]
数组:竞态条件下,累加个各个线程自己的槽Cell[i]
中
-
-
Value = Base + ∑ Cell[i]
6.4 Unsafe类
-
Unsafe 是 Java 中的特殊类,它为 Java 提供了一种底层、"不安全"的机制来直接访问和操作内存、线程
-
Unsafe 类是被 final 修饰的,不允许被继承,并且构造方法为
private
类型,即不允许我们直接 new 实例化。Unsafe 在 static 静态代码块中,以单例的方式初始化了一个 Unsafe 对象,提供了一个静态方法getUnsafe
。 -
在
getUnsafe
方法中,会对调用者的classLoader
进行检查,判断当前类是否由Bootstrap classLoader
加载,如果不是的话就会抛出一个SecurityException
异常,只有启动类加载器加载的类才能够调用 。 -
可以利用反射获得 Unsafe 类中已经实例化完成的单例对象:
public static Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
return unsafe;
}
在 Unsafe 类中,提供了compareAndSwapObject
、compareAndSwapInt
、compareAndSwapLong
方法来实现的对Object
、int
、long
类型的 CAS 操作。以compareAndSwapInt
方法为例:
public final native boolean compareAndSwapInt(Object o,long offset,int expected,int x);
参数中o
为需要更新的对象,offset
是对象o
中整型字段的偏移量,如果这个字段的值与expected
相同,则将字段的值设为x
这个新值,并且此更新是不可被中断的,是一个原子操作。
缓存行伪共享
指的是多个线程同时访问不同的变量,但这些变量在同一个缓存行中,导致了无意义的缓存同步,降低了性能。
-
缓存行:
-
缓存行是处理器缓存中的最小单位,一般大小为64字节(在大多数处理器中)。缓存行的目的是提高内存访问的效率,通过将相邻的内存数据一起加载到缓存中来减少内存访问的延迟。
-
-
伪共享:
-
伪共享指的是多个线程同时修改或访问同一个缓存行中的不同变量,而这些变量可能不是同一个线程关心的,从而导致了无谓的缓存同步操作。。
-
-
性能影响:
-
当多个线程同时修改或访问同一个缓存行中的不同变量时,会导致缓存行在不同核心之间频繁地进行缓存同步,增加了总线通信的开销,降低了程序的性能。
-
-
解决方案:
-
解决缓存行伪共享的常用方法是通过填充(Padding)来确保不同的变量被存储在不同的缓存行中。
-
通过在变量之间插入填充字段,使得每个变量都占据一个独立的缓存行,从而避免了无谓的缓存同步。
-