乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的实现原理包含:volatile关键字和CAS操作。
**官方解释:**volatile修饰的变量,在编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。
否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)
CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 这其实和乐观锁的冲突检查+数据更新的原理是一样的。
程序猿之间没有什么是用代码沟通不了的,如果有那就加上注释。
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
// 获取值并且增加1
public final int getAndIncrement() {
for (;;) { // 这行是一个死循环
// 从内存中读取数据
int current = get();
int next = current + 1;
// 此数据与 (此数据+1 后的结果)进行CAS操作
if (compareAndSet(current, next))
// 如果成功就返回结果,否则重试直到成功为止。
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
// compareAndSet 利用JNI(Java Native Interface)来完成CPU指令的操作
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
在没有锁的机制下,字段value要借助volatile原语,保证线程间的数据是可见性。这样在获取变量的值的时候才能直接读取。然后来看看 ++i 是怎么做到的。
getAndIncrement
采用了CAS操作,每次从内存中读取数据然后将此数据和 +1 后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful
*/
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
注释是这么说的:如果内存中的值跟预期值相等,则原子性更新该值。当成功的时候返回true,失败的时候返回false。
unsafe.compareAndSwapInt
调用native代码实现CAS操作,但是不保证结果成功。这是一项很好的优化,在1.7版本里面如果操作失败会一直占用CPU的资源,而在新版里面我们可以自行决定什么时候不再执行该操作。
先说问题产生的过程,最后给出解决方案:
testList
的变化。public class CAS_ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static List<Boolean> testList = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
int currentSize = testList.size();
System.out.println("t1 操作前列表大小 testList size = " + currentSize);
try {
// TimeUnit.SECONDS.sleep(2);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
exchange(100, 101);
System.out.println("t1 操作后列表大小 testList size = " + testList.size());
if (testList.size() != currentSize + 1) {
System.out.println("testList size期待值是:" + (currentSize + 1) + " testList size 实际值是 " + testList.size() + " 发生了ABA问题");
}
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
int currentSize = testList.size();
System.out.println("t2 操作前列表大小 testList size = " + currentSize);
exchange(100, 101);
exchange(101, 100);
System.out.println("t2 操作后列表大小 testList size = " + testList.size());
if (testList.size() != currentSize + 2) {
System.out.println("testList size期待值是:" + (currentSize + 2) + " testList size 实际值是 " + testList.size() + " 发生了ABA问题");
}
}
});
intT1.start();
intT2.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
intT1.join();
intT2.join();
// 明明线程1先执行,因为某些原因会在线程2操作之后才开始执行,这个时候线程1执行操作
}
/**
* CAS操作更新
*
* @param expect 内存期待值
* @param update 更新后的值
*/
private static void exchange(int expect, int update) {
boolean isSuccess;
isSuccess = atomicInt.compareAndSet(expect, update);
testList.add(isSuccess);
}
}
t2 操作前列表大小 testList size = 0
t1 操作前列表大小 testList size = 0
t2 操作后列表大小 testList size = 2
t1 操作后列表大小 testList size = 3
testList size期待值是:1 testList size 实际值是 3 发生了ABA问题
先说问题产生的过程,最后给出解决方案:
TIPS:1.7版本为了保证操作成功自带死循环,而1.8去掉了死循环,可以参照上面的源码解读。
为了保证一定操作成功,我们为原子操作添加循环判断。
public class CAS_InfiniteLoop {
private static AtomicInteger atomicInt = new AtomicInteger(100);
//维护一个对象引用以及整数“标记”,可以原子方式更新。
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
exchange(100,101);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t2 start");
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 102);
System.out.println("t2 over");
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
}
/**
* CAS操作更新,用do-while操作循环判断保证一定更新
*/
private static void exchange(int expect, int update) {
boolean isSuccess;
do {
isSuccess = atomicInt.compareAndSet(expect, update);
System.out.println("操作:"+isSuccess);
} while (!isSuccess);
}
}
t2 start
t1 start
t2 over
操作:false
操作:false
操作:false……
从打印里面我们可以得出,为了能操作成功线程进入了死循环,这个如果发生在1.7的版本里面基本上是无解的,只能看着CPU资源被浪费。
下面说说正确的使用姿势。
AtomicStampedReference
标记对象,而不直接锁定对象。AtomicStampedReference
利用版本标记更新(每次操作后版本+1)的方式保证操作对象的原子性。如果版本不是预期版本,就算对象是预期值也同样说明了场景已经发生改变,这时候操作失败。public class CAS_RightPosture {
// AtomicStampedReference维护一个对象保证其可以原子方式更新。
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
// stamp = 0
System.out.println("t1 开始执行时 版本是 " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// stamp = 2
System.out.println("t1 睡完一觉 版本是 " + atomicStampedRef.getStamp());
int update = 101;
boolean isSuccess = exchange(100, 101, stamp);
System.out.println("t1 操作是否成功:" + isSuccess + "期望值:" + update + " 更新后的值是:" + atomicStampedRef.getReference());
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int update = 101;
boolean isSuccess = exchange(100, 101, atomicStampedRef.getStamp());
System.out.println("t2 操作是否成功:" + isSuccess + "期望值:" + update + " 更新后的值是:" + atomicStampedRef.getReference());
update = 100;
isSuccess = exchange(101, 100, atomicStampedRef.getStamp());
System.out.println("t2 操作是否成功:" + isSuccess + "期望值:" + update + " 更新后的值是:" + atomicStampedRef.getReference());
}
});
refT1.start();
refT2.start();
}
/**
* CAS操作更新,为了保证一定更新所以用do-while操作循环判断,为了防止进入死循环添加条件当条件满足之后退出任务
*/
private static boolean exchange(int expect, int update, int stamp) {
boolean isSuccess;
int condition = 0;
do {
// 只有期待值和期待版本跟内存中数据一致才更新
isSuccess = atomicStampedRef.compareAndSet(expect, update, stamp, stamp + 1);
if (condition++ > 10) {
System.out.println("失败次数太多,为了不浪费CPU退出任务");
break;
}
} while (!isSuccess);
return isSuccess;
}
}
t1 开始执行时 版本是 0
t2 操作是否成功:true期望值:101 更新后的值是:101
t2 操作是否成功:true期望值:100 更新后的值是:100
t1 睡完一觉 版本是 2
失败次数太多,为了不浪费CPU退出任务
t1 操作是否成功:false期望值:101 更新后的值是:100
AtomicBoolean
标示这个文件是否已经操作过。synchronize加锁只允许单个线程对该文件状态进行读写,乐观锁则不然。原子更新类型 | 名称 | 描述 |
---|---|---|
基本类型 | AtomicBoolean | 原子更新布尔类型 |
基本类型 | AtomicInteger | 原子更新整型 |
基本类型 | AtomicLong | 原子更新长整型 |
数组类型 | AtomicIntegerArray | 原子更新整型数组里的元素 |
数组类型 | AtomicLongArray | 原子更新长整型数组里的元素 |
数组类型 | AtomicReferenceArray | 原子更新引用类型数组的元素 |
数组类型 | AtomicBooleanArray | 原子更新布尔类型数组的元素 |
引用类型 | AtomicReference | 原子更新引用类型 |
引用类型 | AtomicReferenceFieldUpdater | 原子更新引用类型里的字段 |
引用类型 | AtomicMarkableReference | 原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和应用类型 |
字段类型 | AtomicIntegerFieldUpdater | 原子更新整型的字段的更新器 |
字段类型 | AtomicLongFieldUpdater | 原子更新长整型字段的更新器 |
字段类型 | AtomicStampedReference | 原子更新带有版本号的引用类型。 |
CAS和ABA问题
Java多线程3 原子性操作类的使用
AtomicBoolean类实现