管程:悲观锁。无锁:乐观锁
1、CAS
CAS,compareAndSet。调用CAS时,它会检查该属性当前的值是否等于第一个参数,如果等于,就将它替换成第二个参数,并返回true。
它是一个原子操作,底层是一条lock cmpxchg指令,在单核、多核都能保证原子性
多核下如何保证原子性?
- 在多核下,某个核心执行到带lock的指令时,CPU会锁住总线,直到这个核心把指令执行完毕才开启总线。
- 这个过程不会被线程的调度机制打断,即不存在某个线程改变数据的中间状态,只会有两种情况:改了、没改。
示例
AtomicInteger是JDK提供的原子整数类型。
private AtomicInteger balance;
public void withdraw(Integer amount) {
while(true) {
// 需要不断尝试,直到成功为止
while (true) {
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990 那么本线程的这次 990 就作废了,进入 while 下次循环重试 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
- 先获取共享变量当前的值
- 做出自己的计算
- 使用CAS修改共享变量的值。如果共享变量当前的值不等于“第一步时获取的值”,说明有别的线程已经修改了该变量,那么本次CAS操作失败,返回false。乐观锁的思想。
- 注意:
CAS方法是原子的,但之前这个获取值的操作,和最后的CAS之间,可能会发生线程上下文切换
,所以会有CAS失败的情况。
2、CAS必须搭配volatile一起使用
上面的AtomicInteger,源码中使用了volatile的int来保存数据。
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
CAS必须借助volatile才能读取到共享变量的最新值,来实现“比较并交换”的效果。
3、乐观锁的特点
结合CAS和volatile可以实现无锁并发(乐观锁),适用于线程数少、CPU核心数多的情况。活跃线程数不要多于核心数量,否则肯定会发生线程上下文切换。
乐观锁是无锁+失败重试的思想,它和加锁的区别:
- 线程不需要获取锁,也就不会被阻塞。线程可以在时间片内一直重试,减少了额外的线程上下文开销
- 乐观锁适合竞争小的场景。如果竞争激烈,大多数的重试都会再次失败,影响效率,此时应该考虑上锁
乐观锁的问题
- ABA问题,使用版本号解决
- 死循环问题,CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大
- 只能保证一个共享原子变量的操作问题(使用AtomicReference解决,将多个对象放入CAS中操作)
4、原子整数
使用volatile定义一个整数:
volatile int a = 0;
在多线程下执行a++的操作,并不能保证线程安全。volatile只能保证每次都读取到最新的值,线程安全需要CAS来保证
。
JUC并发包提供了一些CAS的工具类:
- AtomicBoolean
- AtomicInteger
- AtomicLong
比如AtomicInteger,它的方法都是原子的
AtomicInteger i = new AtomicInteger();
//获取并自增,类似i++
i.getAndIncrement();
//自增并获取,类似++i
i.incrementAndGet();
//获取并自减,类似i--
i.getAndDecrement();
//自减并获取,类似--i
i.decrementAndGet();
//获取并增加
i.getAndAdd(5);
//增加并获取
i.addAndGet(5);
i.getAndUpdate(value -> value*2);
i.getAndAccumulate(10, (p,x)->p%x);
- getAndUpdate,可以随便对原子整数做运算。如果在lambda中引入局部变量,局部变量必须是final的
- getAndAccumulate,可以通过参数1来引入外部变量,其不在lambda中,不要求final
这些方法底层都是使用了compareAndSet的CAS操作来实现的。
5、原子引用
原子引用类型:
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
原子引用是为了保证,修改引用值时的原子性。即实现对象的替换,而不是实现对象某个属性的修改。
1、AtomicReference
比如需要使用BigDecimal,想要保证它的原子性,就需要使用原子引用类型。
public class DecimalAccountCas implements DecimalAccount{
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
//查看余额
@Override
public BigDecimal getBalance() {
return balance.get();
}
//取款
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)){
break;
}
}
}
}
注意使用步骤:
- 使用AtomicReference类型定义变量,把变量的类型作为泛型
- 定义构造器,给AtomicReference类型赋值,new一个AtomicReference,将变量的数据作为参数传入
- 后续操作变量,就可以使用CAS方法。
2、ABA 问题
使用原子引用时,可能会有ABA问题:
问题描述:
- 一个线程想要把A改成C,中途有线程把A修改成了B,又修改回了A。
- 之前的线程接着执行,发现此时的值依然是A,就成功改成了C。
发生的原因:
-
因为compareAndSet方法的逻辑是,只要当前值等于第一个参数,就能执行成功
-
可能一个线程对引用做了多次修改,但引用的值最后又修改回去了
产生的影响:
- 一般不会造成问题,但是存在隐患。
- 合理的做法是,检测到其他线程对变量做了修改,就放弃这次操作,重新获取当前值,然后尝试CAS。
如何解决:使用AtomicStampedReference类,引入版本号机制。
3、AtomicStampedReference
AtomicStampedReference可以给原子引用加上版本号,可以知道引用变量被修改了几次。
做法是:
- 定义AtomicStampedReference类型,把引用类型作为泛型。后面指定引用变量的初始值、初始版本号。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
2.每个修改操作,同时检查引用和版本号,操作成功就对版本号+1
while (true) {
//获取当前引用值
String prev = ref.getReference();
//获取当前版本号
int stamp = ref.getStamp();
if (ref.compareAndSet(prev, "C", stamp, stamp + 1)){
break;
}
}
- 提前获取引用、版本号
- CAS时,只有当前引用、版本号都一样,才能成功执行
4、AtomicMarkableReference
有时候只需要关注,引用变量是否被修改过,而不关心被修改了几次,就可以使用AtomicMarkableReference。
AtomicMarkableReference使用boolean变量来表示,是否发生过修改。这样用起来比较方便
做法:
- 定义AtomicMarkableReference类型,把引用类型作为泛型。第二个参数代表一个标记
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
-
CAS操作时,比如设置成只有标记为true才能执行,然后把标记置为false
GarbageBag prev = ref.getReference();
ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
6、原子数组
有这些:
- AtomicIntegerArray(数组内部是Integer类型)
- AtomicLongArray(数组内部是Long类型)
- AtomicReferenceArray(数组内部是引用类型)
用法:用AtomicIntegerArray类型替换int[]
new AtomicIntegerArray(10);
7、字段更新器
可以实现原子性修改对象的某个属性值。注意,要修改的字段必须加上volatile,否则会出现异常
- AtomicReferenceFieldUpdater(字段类型是引用)
- AtomicIntegerFieldUpdater(字段类型是Integer)
- AtomicLongFieldUpdater(字段类型是Long)
用法:
public class Dog {
private volatile int field;
public static void main(String[] args) {
Dog dog = new Dog();
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Dog.class, "field");
fieldUpdater.compareAndSet(dog, 0, 10);
}
}
注意:
- 通过AtomicIntegerFieldUpdater类的静态方法newUpdater创建字段更新器对象,传入类的Class文件、要修改的字段名称
9、Unsafe
Unsafe 对象提供了非常底层的操作内存、线程的方法。Unsafe的含义是,比较底层,比较危险,注意:但并不是说这个对象方法是线程不安全的。
Unsafe 对象不能直接new出来,因为它是单例的,只能通过反射获得。
获得Unsafe:
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}