一、锁
实现线程同步最直观的策略便是加锁,如使用synchronized关键字进行加锁实现线程同步最直观的策略便是加锁,如使用synchronized关键字进行加锁。悲观锁:悲观锁在对一个对象进行操作是悲观的认为这个对象会被修改,因此悲观锁在一个线程进行加锁后使得该对象变为该线程的独享对象,此时任何其他的线程都会被悲观锁阻拦在外,无法进行操作。显然悲观锁带来的开销是巨大的,悲观锁存在以下几点缺陷:1、一个线程获得悲观锁后其他线程必须阻塞。2、线程切换时要不停的释放锁和获取锁带来开销。3、当一个低优先级的线程获得悲观锁后高优先级的线程也必须等待,这会导致线程优先级倒置问题。使用synchronized加锁是一种典型的悲观锁。
乐观锁:乐观锁与悲观锁不同,他乐观的认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果是则再试一遍直到成功为止,这个重试的过程被称之为自旋。乐观锁其实并没有加锁,所以不会出现悲观锁的问题,但乐观锁也引入了诸如ABA、自旋次数过多等问题。
二、CAS操作
在JDK1.5之前,Java中的所有锁都是重量级的悲观锁,1.5中引入了java.util.concurrent包,这个包中提供了乐观锁的使用,而整个JUC包实现的基石则是CAS操作。
CAS(compare and swap)即比较和替换,在juc包中进程会看到诸如此类的代码:unsafe.compareAndSwapInt(this, valueOffset, expect, update);这便是使用CAS操作。CAS操作的过程为判断某个内存地址的值是否为给定的原值,如果是则修改为新值并返回成功,否则返回该地址的值。CAS操作有三个参数:内存地址、原值、新值。当内存地址中存放对象等于提供的原值时则将其替换为新值。(在刚刚的compareAndSwapInt中有四个参数,其中this和valueOffset共同提供内存地址)
以AtomicInteger的修改为例查看使用CAS时如何无锁并安全的修改某个值的:
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
先获得要修改的原值prev和要改成的新值next,当使用CAS替换新值不成功时,自旋,重新获得原值和新值再试一次直到成功为止。
这段代码中可以发现两个问题:1、compareAndSet操作必须是原子性的,即操作中间无法被打断。2、获取原值时要保证这个原值对本线程可见。这两个问题也是CAS操作的前提条件。
compareAndSet其实是调用了JNI,使用本地方法来保证原子性,这其实是在硬件层面的保障,许多CPU中是存在如CMPXCHG指令的。
原值的可见性则是使用java中的关键字volatile来保证的。
三、CAS引入的问题
cas虽然通过无锁操作提高了系统的吞吐率,但是也引入了一些问题。
1、ABA问题。
CAS操作的流程为:1、读取原值。2、通过原子操作比较和替换。虽然比较和替换操作是原子性的,但是读取原值和比较替换着两部操作并不是原子性,期间原值可以被其他线程修改。当CAS读取到原值为A时,此时其他线程闯入将值修改为B然后又改回A,在执行比较和替换时CAS操作发现该地址的值为A正常替换,但其实这个值已经被修改过一遍了。ABA问题有些时候对系统不会产生问题,有些时候却是致命的问题,如:你在自动售货机上买了瓶饮料花了5块钱,你的账户原有10块钱,此时售货机发出扣款CAS请求:如果是10元变为5元。但是由于网络问题,这个请求发了两次,如果第一次执行正常将账户变为5元了,那么第二次请求时无法执行的,但是在第一次扣款操作执行完之后有人给你转账5元了,此时你的账户再次变为10元,那么第二遍的扣款请求是可以执行成功的,显然在这种情况下你多花了5块钱。
ABA问题的解决办法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。
2、自旋次数过多
CAS操作在不成功时会重新读取内存值并自旋重试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。
3、只能保证一个变量的原子性
当对一个变量操作时,使用CAS可以保证原子性,但同时对多个变量进行操作时CAS去无能为力了,当然你可以取巧将多个对象放在一个对象中进行操作,在Java中提供了atomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。或者直接使用锁来操作多个对象。