目录
一、并发编程无锁案例
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
//批量启动线程
ts.forEach(Thread::start);
//等待每一个线程执行完
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance() + " cost: " + (end-start)/1000_000 + " ms");
}
}
//实现类
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
}
上述案例很明显,withdraw方法是不安全的,以前通常的选择是在withdraw上加synchronized实现对多线程操作共享变量的阻塞,但是现在要引入CAS乐观锁的思路
class AccountSafe implements Account {
//引用原子整数类
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while (true) {
//获得此刻balance的最新值 1000
Int prev = balance.get();
//在最新值的基础上做减法 990
int next = prev - amount;
//compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回false 表示失败
//比如:
//拿到的最新值1000,别的线程已经做了减法变成了990,那么1000与990做比较发现不一致
//那么本线程的这次next=990就作废了
//进入while 下次循环重试一致,以 next 设置为新值,返回 true 表示成功,这就是CAS操作的原理
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
CAS操作实际上就是拿着线程缓存空间的值和当前的主存中的值做比较,相同证明没有其他线程改变它,则执行对其修改的操作,否则更新缓存空间的值,再与主存中的值做比较等待修改的过程。
-
无锁高效率
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
二、CAS(compareAndSet)
对上述案例的执行图分析
在Java内存模型中,每个线程有一个属于自己的缓存区,而多个线程共享一个主内存,在并发环境中,多个线程从主内存中读取数据缓存到自己的缓存区,当执行写操作时再写入缓存区,因此,CAS涉及的变量为:一个旧值(线程缓存区的值),一个主存值(主内存中的值),一个新值。当一个线程执行该操作时,需要先将旧值和主存值比较,只有当二者相等时,才会执行新值的更新操作,并同时将新值写入主存,即也会更新主存值。当一个线程执行CAS操作时,另一个线程会自旋等待,直到执行到CAS操作
-
CAS和volatile的关系
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
来看看AtomicInterger内是怎么使用volatie的
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
可以很明显地看出,在原子类的初始化的时候,值是赋给了一个被volatile修饰的value,保证时刻获取到的都是主存中的最新值。
-
CAS的特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗,也是自旋锁的思想。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生(自旋)