常用锁
- 乐观锁(无锁)
- Lock(显示锁)
- synchronized(内置锁)
一、乐观锁
实现方式:CAS(compare and swap:比较并交换),就是在更新一个变量之前,先获取这个变量是否和预期相等,如果相等则更新,否则什么也不做。
例如:
- 数据库更新:update user set name=xxx where xxx=xxx,只有当where符合条件才更新,否则什么也不做。
- Atomic操作
AtomicInteger底层源码
我们在多线程并发i++时,通常会使用atomicInteger.incrementAndGet();来保证线程安全。
来看incrementAndGet方法源码【以i增1为例,i当前是6】:
- 首先获取当前i的值(6)
- 然后计算我增1之后应该是多少(7)
- 然后比较当前的值+1和我计算的7是否相等
- 如果相等则直接修改值,如果不相等则while循环一直等,直到相等。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS存在的问题
- 每一次比较和交换时,都没有得到修改的机会,达到一定次数时如果还没有停下来,则会一直浪费CPU,导致CPU占用非常高
- 无法实现复杂场景下的线程安全,比如:i*5+6
- ABA问题
ABA问题及解决
A->B->A,最终结果仍然为A,CAS发现不了中间B的修改过程。
- 八大基本数类型不存在ABA问题,因为变了就是变了,不代表什么。
- 只有引用类型才存在ABA问题,比如银行搞活动,存款满10万赠送一袋大米,当月仅赠送一次,但不解决ABA问题,每次你取出去2万,再存2万,又到了十万,银行就又要给你一袋大米,因此这个转入转出的过程必须记录,才能解决ABA问题。
解决方式:AtomicStampedReference类,其提供了compareAndSet方法,可以:以版本号或者时间戳的方式,对比要更新的版本号是否等于预期的版本号,如果等于,则以原子方式更新,否则什么也不做。
创建银行账户实体,包含账户名及转账金额两个字段,初始张三的原始金额为8万
public static class BankAccount {
private String name;// 账户名
private Integer money;// 转账金额(转入或转出)
}
BankAccount zhangSan = new BankAccount("张三", 8);// 初始张三的原始金额为8万
ABA问题代码演示
首先使用未解决ABA问题的原子类:AtomicReference
AtomicReference<BankAccount> bankAccountAtomicReference = new AtomicReference<>(zhangSan);
AtomicInteger atomicInteger = new AtomicInteger(0);// 计数
for (int i = 0; i < 100; i++) {
// 入账,每次2万
new Thread(() -> {
BankAccount oldAccount = bankAccountAtomicReference.get();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);
boolean result = bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
if (result && bankAccountAtomicReference.get().money >= 10) {
// 计数,计算兑换了多少袋大米
atomicInteger.incrementAndGet();
// 只要余额>=10万,就送大米,无法判断之前有没有送过
System.out.println("领一袋大米");
}
}).start();
// 出账,每次2万
new Thread(() -> {
BankAccount oldAccount = bankAccountAtomicReference.get();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
}).start();
}
Thread.sleep(1000);
System.out.println("成功兑换了:" + atomicInteger.get() + "袋大米,可真鸡贼");
运行结果【多拿了人家好多袋大米】:
可以看出,100次的来回入账出账,这个鸡贼的客户一不小心领了银行89袋大米,银行亏大了!但细心的朋友会发现,为什么是89袋而不是100袋?这是因为在并发过程中,CAS操作时可能会有多个线程获取到同一个值,导致只能有一个线程更新成功,解决办法就是加上死循环一直等,类似AtomicInteger.incrementAndGet()源码,上文分析过,直接上case:
AtomicReference<BankAccount> bankAccountAtomicReference = new AtomicReference<>(zhangSan);
AtomicInteger atomicInteger = new AtomicInteger(0);
for (int i = 0; i < 100; i++) {
// 入账,每次2万
new Thread(() -> {
// 增加死循环,防止线程冲突CAS失败,失败后一直等直到CAS成功,释放线程
while (true) {
BankAccount oldAccount = bankAccountAtomicReference.get();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);
boolean result = bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
if (result && bankAccountAtomicReference.get().money >= 10) {
// 计数,计算兑换了多少袋大米
atomicInteger.incrementAndGet();
// 只要余额>=10万,就送大米,无法判断之前有没有送过
System.out.println("领一袋大米");
break;// 释放线程
}
}
}).start();
// 出账,每次2万
new Thread(() -> {
BankAccount oldAccount = bankAccountAtomicReference.get();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
}).start();
}
Thread.sleep(1000);
System.out.println("成功兑换了:" + atomicInteger.get() + "袋大米,可真鸡贼");
运行结果【这次数量正确啦~~】:
ABA解决问题代码演示
然后使用解决了ABA问题的原子类:AtomicStampedReference
AtomicStampedReference<BankAccount> bankAccountAtomicStampedReference = new AtomicStampedReference<>(zhangSan, 8);
int stamp = bankAccountAtomicStampedReference.getStamp();
for (int i = 0; i < 100; i++) {
// 入账,每次2万
new Thread(() -> {
BankAccount oldAccount = bankAccountAtomicStampedReference.getReference();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);
boolean result = bankAccountAtomicStampedReference.compareAndSet(oldAccount, newAccount, stamp, stamp + 1);
if (result && bankAccountAtomicStampedReference.getReference().getMoney() >= 10) {
// 只有首次变成10万时,才送大米,之后因为有stamp的匹配限制,后面的10万均不在活动范围内
System.out.println("领一袋大米");
}
}).start();
// 出账,每次2万
new Thread(() -> {
BankAccount oldAccount = bankAccountAtomicStampedReference.getReference();
BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
bankAccountAtomicStampedReference.compareAndSet(oldAccount, newAccount, stamp, stamp + 1);
}).start();
}
运行结果【就第一次到10万的时候拿了一袋,没毛病!】:
二、Lock
常用:ReentrantLock(底层原理:CAS)
可能实现方法:
- 当CAS没成功时,采用while死循环进行自旋,直到获取到锁
- 方法1太占CPU,可否采用wait方法?不可以,wait必须配合synchronized使用,实现一把锁不可能采用另一把锁
- 死循环中采用yield线程让步+自旋?当竞争只有两个线程时可以采用此方法,但线程比较多,下一次轮到谁具有太多的不确定性,无法控制
- 死循环中采用sleep+自旋?sleep多久不确定
- 死循环中采用park+自旋?可!LockSupport.park,底层是unsafe类提供的native方法park,意思是让某个线程睡眠,需要使用unpark手动叫醒。
三、synchronized
具体可参考我的其余两篇文章
四、常见问题
synchronized是重量级锁吗?
原来是,现在不全是。只有发生了OS调用的才是重量级锁,因为需要用户态到内核态的切换。用户态无法操作线程的阻塞及唤醒。
当年为什么会出现ReentrantLock?
Doug Lea,这哥们当年觉得synchronized太慢了,于是自己写了个锁,后来Sun公司觉得它很牛逼,就把它收录到了JDK里面(JDK1.5)。然后Sun觉得我自带的被你干下去了,我一定要超过你,然后在JDK1.6把synchronized进行了升级
ReentrantLock的park操作是否需要用户态切换到内核态?
需要