以一段取款餘額引出問題
- 賬戶餘額提取問題
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無鎖併發,無阻塞併發:
- 因爲不需要進行線程的上下文切換,所以效率很高
- 但是在競爭激烈的情況,頻繁的重試,反而影響效率