Volitale
并发中,锁主要提供两种特性:互斥 与 可见
并发情况下,一个线程先到来,给对象上锁,其他线程只能在后面排队,这种互斥带给我们的是操作的 原子性,即在并发的情况下,一个操作不会受其他线程的影响
另外,每个线程在读取变量时,自己都会缓存一份,也就是说,可能下次读取操作便不会经过内存,而是直接取缓存,当然,这在并发条件下,线程共享的变量会有问题是理所当然的,如果每个线程在访问一个变量时,每次都从内存中重新读取值,那么我们认为这个变量对于每个线程来说具有 可见性
当然,若你不需要互斥,只需要保证可见性,Java 提供了一种更轻量的语法 volitale,经过 volitale 修饰的变量,可以保证:
- 可见性
- 防止指令重排
在执行代码时,代码顺序为 A -> B -> C,CPU 为了执行效率,可能会将其顺序打乱,当然,为什么我们在单线程条件下执行却是有序的,原因是指令重排遵循 as-if-serial 语义,即无论如何重排,都不会影响结果,就像如果 C 依赖于 A 和 B,那么总是会在 A 和
B 都执行完后,才会执行 A
显而易见的是 volitale 并不支持原子性,就像 i++ 的执行过程:
- 从内存中取出 i
- 将 i 加 1
- 再将结果赋值给 i
volitale 有一种写法叫 读 - 写锁,它本身是矛盾的,在我们使用这种写法时,我们认为读取应该可以是不严谨的,如果超越了该模式的最基本应用,结合这两个竞争的同步机制将变得非常困难
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
Unsafe
正如它的名字一样,Unsafe 是一个非常不安全的类
并发包下 Unsafe 提供以下功能:
- 直接操作内存地址
- 恢复与挂起线程
- 开放汇编、CPU级别的 CAS [1] 操作
我们可以认为,Jdk 并发包是在 Unsafe 下建立的,它的构造函数是被类加载器封死的,外包下可以通过反射获取,但使用 Unsafe 类,请务必对它十分清楚
Atomic 系列类
如果我们想为自己的对象原子化,AtomicReference 是一种很好的选择,AtomicReference 内部已经维护好了一个 Unsafe、内存偏移地址,也开放了一些原子操作的入口
public class AtomicReference<V> implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile V value;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// ...
}
如果我们想为自己的对象数组原子化,AtomicReferenceArray 是一种很好的选择
如果我们想为自己的数字变量原子化,可以选择一系列的 AtomicInteger,AtomicDouble ...
它们的实现大致相同,内部都是基于 Unsafe 构建的,我们在这里就不一一详细讨论了