Java并发编程-无锁
2019-04-04 本文已影响0人
agile4j
- 对于并发控制而言,锁是一种悲观策略。它总是假设每次临界区操作都会产生冲突,因此如果有多个线程同时访问临界区资源,就会宁可牺牲性能也要让线程等待,所以说锁会阻塞线程执行。
- 而无锁是一种乐观策略。它会假设对资源的访问是没有冲突的,所有线程都可以在不停顿的状态下执行。如果遇到冲突,就会使用比较交换(CAS CompareAndSwap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作直到没有冲突为止。
1.比较交换(CAS)
1.CAS的优缺点
- CAS的缺点:
- 会增加代码复杂度
- CAS的优点:
- 因其非阻塞性,从而对死锁问题天生免疫
- 线程间的相互影响远小于基于锁的方式
- 不会有锁竞争和系统带来的系统开销
2.CAS的算法过程
- CAS算法包含三个参数CAS(V,E,N)
- V表示要更新的变量
- E表示预期值
- N表示新值
- 算法过程:仅当V值等于E值时,才会将V的值设为N。如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS会返回当前V的真实值。
- 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并更新成功,其余均会失败。失败的线程不会被挂起,仅是被告知失败,此时可以进行重试,也可以放弃操作。
- 在硬件层面,大部分的现代处理器都支持原子化的CAS指令。在JDK5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
2.基于CAS的、无锁的线程安全整数:AtomicInteger
-
JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型,比如说AtomicInteger。可以把AtomicInteger看做一个整数,但与Integer不同的是:AtomicInteger是可变的,并且是线程安全的。对其进行修改等任何操作,都是用CAS指令进行的。
-
AtomicInteger的一些主要方法:
-
就内部实现来说,AtomicInteger保存了一个核心字段:
private volatile int value; // 保存了AtomicInteger的当前实际值
- 此外还有另一个核心字段,它是AtomicInteger实现的关键,后文再详细说明:
private static final long valueOffset; // 保存着value字段在AtomicInteger对象中的偏移量
-
这里我们关注一下incrementAndGet()方法的内部实现(在JDK1.7中的实现,与JDK1.8的并不相同):
-
上述代码中的for循环,就是CAS操作的基本原理。(CAS自旋)
3.Java中的指针:Unsafe类
- 刚才我们看到了incrementAndGet()方法在JDK1.7中的实现,接下来看下在JDK1.8中的实现:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
-
在这里我们看到一个特殊的变量unsafe,它是sum.misc.Unsafe类型。
-
从名字看,这个类封装的是一些不安全操作。这里的不安全,值得就是一些类似指针的操作。之所以不安全是因为如果指针指错了位置,或者计算偏移量时出错,就可能会覆盖别人的内存,导致系统崩溃。
-
Unsafe类提供了很多使用CAS原子指令实现的方法,例如:
-
由此可见,虽然Java抛弃了指针,但在一些关键时候,类似指针的技术还是必不可少的。但不行的是,JDK开发人员并不希望大家使用这个类。获得Unsafe实例的方法是调动其工厂方法getUnsafe(),但它的实现是这样的:
-
JDK1.7
- JDK1.8
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
- 两个版本的算法一样只是实现不同。这里以JDK1.7版本为例进行说明:根据Java类加载器的工作原理,应用程序的类由APPLoader加载。而系统核心类,如rt.jar中的类由BootStrap类加载器加载。BootStrap加载器没有相应的Java对象,因此试图获得这个类加载器会返回null。所以,当一个类的类加载器为null时,说明它是由BootStrap加载的,而这个类也极有可能是rt.jar中的类。
4.无锁的对象引用:AtomicReference
- AtomicReference和AtomicInteger类似,但它是对普通对象引用的封装。
- 虽然AtomicReference可以保证在修改对象引用时的安全性,但它仍有一个有关原子操作的逻辑上的不足。之前提过,判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的,但也有可能出现一个意外,那就是当获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过这两次修改后,对象的值又恢复为旧值。这样当前就无法正确判断出这个对象究竟是否被修改过。图示如下:
- 一般来说,这种情况发生的概率很小,而且即使发生了,如果修改的对象是没有过程的状态信息,所有信息都只保存于对象的数值本身,也不会引起问题。但现实中,还有另外一种场景,就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。JDK也已经考虑到了这种情况,提供了AtomicStampedReference来解决这个问题,详见下一小节。
5.带有时间戳的对象引用:AtomicStampedReference
- AtomicReference无法解决上述问题的根本原因是:对象在修改过程中丢失了状态信息。对象值本身和状态被画上了等号。因此,只要能够记录对象在修改过程中的状态值,就可以很好地解决对象被反复修改导致线程无法正确判断对象状态的问题。
- AtomicStampedReference也正是这么做的。它内部不仅维护了对象值,还维护了一个时间戳(这里称为时间戳,实际上可以使用任何一个整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才成功。因此,即使对象值被反复读写,写会原值,只要时间戳发生变化,就能防止不恰当的写入。
-
AtomicStampedReference的几个API在AtomicReference的基础上新增了有关时间戳的信息:
6.无锁的原子数组:AtomicIntegerArray
- 除了提供基本数据类型外,JDK还提供了数组等复合结构。当前可用的原子数组有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别表示整数数组、long型数组和普通的对象数组。
- 这里以AtomicIntegerArray为例,说明原子数组的使用方式。
- AtomicIntegerArray本质上是对int[]类型的封装,使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:
7.让普通变量也享受原子操作:AtomicIntegerFieldUpdater
- 有时候,由于初期考虑不周或后期需求变动,一些普通变量也可能会有线程安全化的需求。为了避免大范围的代码改动,JDK提供了一个实用的工具类:AtomicIntegerFieldUpdater。它可以让你在不改动(或极少改动)原有代码的基础上,让普通的变量也享受CAS操作带来的线程安全性。
- 根据数据类型不同,这个Updater有三种,分别是AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。顾名思义,它们分别可以对int、long和普通对象进行CAS操作。
- 虽然AtomicIntegerFieldUpdater很好用,但还是有几个注意事项:
- Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错,比如将变量声明为private,就是不可行的。
- 为了确保变量被正确的读取,它必须是volatile类型的。如果原有代码中未声明为这个类型,那么简单地声明一下就行,这不会引起任何问题。
- 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)
END
参考资料:《实战Java高并发程序设计》