Java实现CAS乐观锁、自旋锁
介绍CAS操作前,我们先简单看一下乐观锁 与 悲观锁这两个常见的锁概念。
悲观锁:
从Java多线程角度,存在着“可见性、原子性、有序性”三个问题,悲观锁就是假设在实际情况中存在着多线程对同一共享的竞争,所以在操作前先占有共享资源(悲观态度)。因此,悲观锁是阻塞,独占的,存在着频繁的线程上下文切换,对资源消耗较大。synchronized就是悲观锁的一种实现。
乐观锁:
如名一样,每次操作都认为不会发生冲突,尝试执行,并检测结果是否正确。如果正确则执行成功,否则说明发生了冲突,回退再重新尝试。乐观锁的过程可以分为两步:冲突检测 和 数据更新。在Java多线程中乐观锁一个常见实现即:CAS操作。
CAS
CAS,(Compare-And-Swap,比较和替换)。其具有三个操作数:内存地址V,旧的预期值A,新的预期值B。当V中的值和A相同时,则用新值B替换V中的值,否则不执行更新。(PS:上述的操作是原子性的,因为过程是:要么执行更新,要么不更新)
新建一个接口,统一加锁入口:
public interface MyCAS {
void doSomething();
}
加锁工具类:
import java.util.concurrent.atomic.AtomicReference;
public class MyCASUtil {
//封装为原子类
private static final AtomicReference<Thread> atomicThread = new AtomicReference<Thread>();
//执行类
public static void doSomethingCAS(MyCAS myCAS) {
lock();
myCAS.doSomething();
unLock();
}
//前置方法 加锁
private static void lock() {
//获取当前线程
Thread thread = Thread.currentThread();
while (!atomicThread.compareAndSet(null, thread)){
//稍微一等 节省cpu开销
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//后置方法 解锁
private static void unLock() {
Thread thread = Thread.currentThread();
atomicThread.compareAndSet(thread, null);
}
}
其中的compareAndSet()方法是一个原子操作, 当current值符合expect时,用next替换了current。如果不符合,则在循环中不断尝试,直到成功为止。
在这个例子中我们要怎么使用呢? 我们可以新建一个类实现MyCAS这个接口,把需要加锁的代码放在方法doSomething中,使用方法举例:
public class MyCASTest implements MyCAS {
private int val;
MyCASTest(int val) {
this.val = val;
}
public void doSomething() {
System.out.println(val);
try {
Thread.sleep(100);
val++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试
public class MainTest {
public static void main(String[] args) {
final MyCAS casTest = new MyCASTest(0);
ExecutorService executor = newFixedThreadPool(10);
for (int i = 0; i < 100;i++) {
executor.execute(new Runnable() {
public void run() {
//加锁的情况
MyCASUtil.doSomethingCAS(casTest);
//不加锁的情况
//casTest.doSomething();
}
});
}
executor.shutdown();
}
}
CAS存在的问题
- ABA问题
ABA问题值,内存地址V的值是A,线程one从V中取出了A,此时线程two也从V中取出了A,同时将A修改为B,但是又因为一些原因修改为A。而此时线程one仍看到V中的值为A,认为没有发生变化,此为ABA问题。解决ABA问题一种方式是通过版本号(version)。每次执行数据修改时,都需要带上版本号,如:1A,2B,3A。通过比较版本号可知是否有发生过操作,也就解决了ABA问题。在我们这个例子中并不存在ABA问题。
- 未知的等待时长
因为CAS采取失败重试的策略,所以不确定会发生多少次循环重试。如果在竞争激烈的环境下,其重试次数可能大幅增加。此时效率也就降低了。