无锁
实现并发的几种方式:
1. 锁
2. CAS
3. ThreadLocal变量;可重入性代码
- 锁是一种悲观的策略,锁认为每次临界区的操作都会产生冲突,所以当有多个线程需要同时访问临界区资源,锁会阻塞线程执行
- 无锁是一种乐观的策略,它假设所有对资源的访问都是没有冲突的,因此所有线程都可以不用阻塞的持续执行,一旦遇到冲突,无锁采用CAS(Compare And Swap)来鉴别冲突,一旦检测到冲突,则重试当前操作直到没有冲突为止
交换技术(CAS)
CAS有两大好处
- 无锁的方式没有锁竞争带来的系统开销,也没有不同线程间频繁调度的开销,所以性能比锁更优越
- 对死锁免疫
具体思想:
CAS包含三个参数(V,expect,update),我认为这三个参数分别是:
V表示当前内存中的值,而expect表示当前读取到的值,只有当当前读取到的值和内存中的值一致时,我们会把内存中的时更新为最新的值,即update;若不等,则说明该值被其他线程修改了
每次CAS操作时,一定有一个线程会成功更新变量,而其他失败的线程不会挂起,而是允许继续重试,直到成功为止
无锁的线程安全整数:AtomicInteger
>在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,
>该包下的类都是使用CAS实现的
>在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全,实现了一些直接使用CAS操作的线程安全的类型
>下面以AtomicInteger为例,来看一下是如何实现的。
我们列举AtomicInteger下的incrementAndGet()方法,该方法使得当前值加1,返回新值:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
该方法incrementAndGet()方法实现了原子性的自增操作(++i不是原子性),内部就是使用CAS技术实现的
Volatile变量
private volatile int value;
代表AtomicInteger的当前实际取值,此外还有一个: private static final long valueOffset; 它保存着value字段在AtomicInteger对象中的偏移量。
Compare And Swap
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 注意是静态的
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。
public final native boolean compareAndSwapInt(Object o,long offset,int expected,int x);
compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,Offset - value属性在内存中的位置(需要强调的是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
这样我们再来看之前提到的incrementAndGet()方法,循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新值,否则重试到成功为止。这里可能存在一个隐患,那就是循环时间过长,总是在当前线程compareAndSet时,有另一个线程设置了value(点子太背了),这个当然是属于小概率时间,目前Java貌似还不能处理这种情况。
CAS缺点
- ABA问题:当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过两次修改后,对象的值又恢复为旧值,这样当前线程无法正确判断这个对象是否修改过
- 解决办法:JDK1.5可以利用类AtomicStampedReference来解决这个问题,AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳,对象值和时间戳都必须满足期望值,写入才会成功
- 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
- 解决办法:JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性
- 解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。