以一段取款余额引出问题
- 账户余额提取问题
public interface Account {
public static void main(String[] args) {
// 不安全 无锁
Account accountUnsafe = new AccountUnsafe(10000);
Account.demo(accountUnsafe);
}
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
//创建1000个线程
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 this.balance;
}
@Override
public void withdraw(Integer amount) {
this.balance -= amount;
}
}
//执行
public static void main(String[] args) {
// 不安全 无锁
Account accountUnsafe = new AccountUnsafe(10000);
Account.demo(accountUnsafe);
}
结果: 250 cost: 197 ms
结论:1000个线程对账号为10000余额扣减10,结果应为0。因为出现了共享资源的竞争问题,多线程导致结果出错。
解决方式-加锁
class AccountSynchronized implements Account{
//余额
private Integer balance;
public AccountSynchronized(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
synchronized (this){
this.balance -= amount;
}
}
}
//执行
public static void main(String[] args) {
// synchronize 加锁
Account accountSynchronize = new AccountSynchronized(10000);
Account.demo(accountSynchronize);
}
结果: 0 cost: 209 ms
结论:结果正确
解决方式-无锁
class AccountCas implements Account{
//余额
private AtomicInteger balance;
public AccountCas(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
//CPU 指令级别 原子操作不可分割
while(true){
int prve = balance.get();
int next = prve - amount;
// CAS 比较并设置 机制:会以prve与当前最新的balance值作比较,如果过相同则将值设置为next
// 若失败则不断进行尝试
if(balance.compareAndSet(prve,next)){
break;
}
}
}
}
//执行
public static void main(String[] args) {
// CAS 无锁
Account accountCas = new AccountCas(10000);
Account.demo(accountCas);
}
结果: 0 cost: 279 ms
结论:结果正确
Cas与volatile
- CAS
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while(true){
//获取旧值100
int prve = balance.get();
// next = 100 - 10 = 90
int next = prve - amount;
// CAS 比较并设置 机制:会以prve与当前最新的balance值作比较,如果过相同则将值设置为next
// compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
//不一致了,next 作废,返回 false 表示失败
//比如,别的线程已经做了减法,当前值已经被减成了 90
//那么本线程的这次 90 就作废了,进入 while 下次循环重试
//一致,以 next 设置为新值,返回 true 表示成功
if(balance.compareAndSet(prve,next)){
break;
}
}
}
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
注意 cas底层是在cpu指令上lock cmpxchg,在单核cpu与多核cpu都能保证【比较-交换】的原子性。
在多核的cpu下,某一个核执行带lock的指令,CPU会让总线锁住,当这个核把指令执行完毕,在开启总线。这个过程中指令的执行不会被线程调度机制锁打断,保证多线程对内存操作的原子性。
- volatile
获取共享变量时,为了保持可见性需要使用volatile
volatile可以修饰成员变量与静态成员变量,防止变量从工作缓存中获取变量,必须从主存中获取变量,线程操作volatile变量直接操作主存,即线程对volatile修改对另一个线程可见
注意: volatile只能解决线程的可见性问题,不能解决指令交错问题(不能保证原子性)
Cas必须使用volatile变量 保证共享变量的可见性,才能实现【比较与交换】
以AtomicInteger为例:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 内部维护了value 被volatile所修饰
private volatile int value;
无锁效率相对高
- 在无锁的状态下,即使重试失败,线程始终处于高速的运行状态下,而使用sychronied会让线程在没有锁的情况下,上下文切换,进入阻塞。
- 举个例子:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,
等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大 - 在无锁的情况,保持线程的运行,需要额外的CPU支持。CPU相当于跑道,线程的运行无从谈起,虽然不会进入阻塞,但是由于没分到时间片,线程处于可运行状态,但是依然会导致线程的上下文切换
CAS的特点
结合CAS与volatile的特点,无锁使用与线程数少,CPU核数多的情况
- CAS无锁为基于乐观锁思想:乐观的估计,不怕其他线程修改共享变量,就算改了,在进行重试
- Synchronized基于悲观锁思想:悲观的估计,防止其他线程修改共享变量,修改完成后在解锁。
- CAS无锁并发,无阻塞并发:
- 因为不需要进行线程的上下文切换,所以效率很高
- 但是在竞争激烈的情况,频繁的重试,反而影响效率