乐观锁与悲观锁
乐观锁与悲观锁是一种思想:
- 悲观锁:悲观的认为当前环境并发严重,所以需要将共享资源锁住,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
如synchronized的实现 - 乐观锁:认为当前环境并发不严重,每次去拿数据的时候都认为别人不会修改,所以不上锁,共享资源所有线程都可以使用,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据
如版本号机制和CAS算法实现
很明显,乐观锁适合多读场景,悲观锁适合多写场景
CAS算法
CAS:Compare And Swap (Compare And Exchange),可以叫自旋锁或无锁
CAS是一种非阻塞的同步方式,基于内存位置(V)、预期原值(A)和新值(B)三个操作数
一个值+1的操作,如果通过CAS算法,其步骤为:
- 内存位置值V=10,读取内存地址值A=V=10
- 更新操作,计算新值B=A+1=11
- 再次读取V值,比较A与V,如果不等,说明有其他线程修改了,即重新读取V并赋值给A
- 相等的话就可以更新V值,V=B=11
可见,当多线程修改了V值时,当前会在第三步第一步自旋,所以叫自旋锁
自旋锁在 JDK1.4中引入,使用 -XX:+UseSpinning 来开启。JDK 1.6 中变为默认开启,并且引入了自适应的自旋锁(由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定最大自旋次数)
那么CAS能处理高并发情况吗?
如果在比较完A与V值,后续更新V值时V值被修改了怎么办?
后续解决
ABA问题
ABA问题:线程2将V值修改,然后再改回,即A->B->A,CAS算法并不能察觉到修改
如果是基本数据类型,ABA问题不会造成影响,但如果是链表等复杂对象,就会出问题:
一个链表:线程1知道链表为A->B,希望丢掉A,将链表设置为B(即链表头为B)
这个时候的CAS算法中V是A结点,新值为B结点
当线程1进行到CAS第三步前,线程2将链表修改为A->C->D,B为游离状态
CAS算法发现V依旧是A结点,即算法通过,将V改成了B结点,这造成丢弃了C、D结点数据
如何解决ABA问题?
可以通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就知道数据被修改了,修改操作失败
CAS的缺陷
- ABA问题:CAS算法不知道数据的ABA修改
- CPU开销过大:许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力
synchronized锁是阻塞队列,一个线程占有资源后其他线程直接阻塞;而CAS是所有线程一直在自旋,所以CAS强调多读场景 - 不能保证代码块的原子性:CAS算法是通过比较V与A判断是否被其他线性修改,如果同时需要保证多个变量同时更新(代码块的原子性),CAS无法做到
当然,选择使用CAS算法是就要考虑是否为多读场景,不然换悲观锁实现
Java中的自旋锁向重量级锁升级机制
jdk1.6之前是自旋超过10次,或者等待线程超过cpu核数的2分之一,自动升级
jdk1.6以后是自适应自旋锁,它会自己判断什么时候升级
Java中的CAS乐观锁
Java中提供了原子操作类,java.util.concurrent.atomic包下的类,正是基于CAS算法实现
我们模拟一个多线程的情况(通过线程sleep)
public class T0 {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
//每个线程让count自增100次
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
}
结果为140,不是我们想象中的200
因为count++并不是原子操作
可以通过AtomicInteger类完成原子操作
public class T0 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
}
解析incrementAndGet方法
-
AtomicInteger类 进入 Unsafe类(不安全类-会涉及到直接内存访问)
-
getAndAddInt方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
do-while操作:一直往上递增,直到compareAndSwapInt为true
compareAndSwapInt就是基于CAS算法,它是native实现,通过c/c++实现的
去HotSpot中找源码,jdk8的unsafe.cpp:Unsafe_CompareAndSwapInt方法本质上是调用了cmpxchg方法
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
cmpxchg方法中有汇编操作,cmpxchg = compare and exchange
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
LOCK_IF_MP = LOCK_IF_Multi Processors
如果是多个CPU,执行lock cmpxchgl 指令
,单个CPU执行cmpxchgl
指令即可
存在汇编级指令cmpxchgl,其就是CAS算法
如果在比较完A与V值,后续更新V值时V值被修改了怎么办?
即这个汇编指令lock cmpxchgl 指令
,cmpxchgl 是CAS算法的汇编指令,本质上是非原子性的,靠的是lock指令
lock汇编指令是在执行后面指令的时候锁定一个北桥信号,这样就可以避免上述问题