標題優(yōu)化方法百度網(wǎng)站免費優(yōu)化軟件下載
同步是什么?
當兩個線程同時對一個變量進行修改時,不同的訪問順序會造成不一樣的結(jié)果,這時候就需要同步保證結(jié)果的唯一性。
未同步時
新建Bank類,transfer()用于在兩個賬戶之間轉(zhuǎn)賬金額
class Bank {private double[] accounts;public Bank(int accountNum, double initialMoney) {accounts = new double[accountNum];Arrays.fill(accounts, initialMoney);}public void transfer(int from, int to, double money) {if (accounts[from] < money) {return;}System.out.print(Thread.currentThread());accounts[from] -= money;System.out.printf("%10.2f from %d to %d", money, from, to);accounts[to] += money;System.out.printf(" Total %10.2f%n", getTotal());}public double getTotal() {double total = 0.0d;for (double temp : accounts) {total += temp;}return total;}public int size() {return accounts.length;}
}
假設銀行有1000個開戶人,每個人賬戶有1000元,新建1000個線程進行隨機轉(zhuǎn)賬,無論怎么轉(zhuǎn)賬,總金額都應該為1000000,但實際的錢卻越來越少(這個例子不太好,原因是浮點數(shù)加減可能誤差)
int NACCOUNTS = 1000;
double INITIAL_BALANCE = 1000;
double MAX_AMOUNT = 1000;
int DELAY = 10;Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {int fromAccount = i;new Thread(new Runnable() {@Overridepublic void run() {try {while (true) {int toAccount = (int) (bank.size() * Math.random());double money = MAX_AMOUNT * Math.random();bank.transfer(fromAccount, toAccount, money);Thread.sleep((int) (DELAY * Math.random()));}} catch (InterruptedException e) {e.printStackTrace();}}}).start();
}
原因分析:
- 當兩個線程同時執(zhí)行到accounts[to] += money;
- 線程1取出accounts[to](如900)放到寄存器,+money(如100),正準備寫回accounts[to](變成了1000)時
- 線程2搶占到權限執(zhí)行accounts[to] += money 將accounts[to]修改為了10002
- 線程1再寫就將10002覆蓋成了1000
ReentrantLock
修改Bank,對其transfer方法加鎖,注意lock應該為成員變量,即每個Bank實例都只有一把鎖,不同對象之間的鎖不會互相影響,需要在finally釋放鎖
class Bank {private double[] accounts;private ReentrantLock lock = new ReentrantLock();public Bank(int accountNum, double initialMoney) {accounts = new double[accountNum];Arrays.fill(accounts, initialMoney);}public void transfer(int from, int to, double money) {if (accounts[from] < money) {return;}lock.lock();try {System.out.print(Thread.currentThread());accounts[from] -= money;System.out.printf("%10.2f from %d to %d", money, from, to);accounts[to] += money;System.out.printf(" Total %10.2f%n", getTotal());} finally {lock.unlock();}}public double getTotal() {double total = 0.0d;for (double temp : accounts) {total += temp;}return total;}public int size() {return accounts.length;}
}
Condition
在轉(zhuǎn)賬時,應該避免選擇沒有足夠資金的賬號轉(zhuǎn)出
if(bank.getMoney(fromAccount) >= money){bank.transfer(fromAccount, toAccount, money);
}
但在多線程情況下,可能兩個線程同時進入if,一個線程轉(zhuǎn)賬后導致另外一個線程金額不夠轉(zhuǎn)賬,故我們要確保在檢查之后加鎖禁止別的線程先行一步
private ReentrantLock lock = new ReentrantLock();
lock.lock();
try {while(account[from] < money){}
}finally{lock.unlock();
}
但當賬戶沒有錢的時候,轉(zhuǎn)出線程會一直等待其他線程轉(zhuǎn)入資金,而其他線程因為無法拿到鎖而無法轉(zhuǎn)入,這就造成了死鎖,這時候就需要條件對象,當金額不足時阻塞線程放棄鎖的持有
private ReentrantLock lock = new ReentrantLock();
private Condition moneyEnough;lock.lock();
try {moneyEnough = lock.newCondition();while(account[from] < money){moneyEnough.await();}
}finally{lock.unlock();
}
當其他線程轉(zhuǎn)賬后,應該調(diào)用signalAll()喚起所有await()中的線程,當其中的某個線程被調(diào)度并再次獲取鎖后,會再進入try子句檢測金額是否足夠
moneyEnough.signalAll();
Tips:
- 還有一個signal()方法隨機喚起一個等待線程
- 當所有線程的金額都小于轉(zhuǎn)賬金額,調(diào)用await(),所有線程都會阻塞,此時會再次死鎖
synchronized
Java中每一個對象都有一個內(nèi)部鎖,如果一個方法用synchronized聲明,線程調(diào)用該方法時需要獲得其內(nèi)部鎖,即
public synchronized void method(){}
等價于
private ReentrantLock innerLock = new ReentrantLock();
public void method(){innerLock.lock();try{}finally{innerLock.unlock();}
}
內(nèi)部鎖只有一個條件,對其的阻塞和喚醒調(diào)用wait()、notifyAll()/notify(),它們是Object中的final方法,相當對ReentrantLock調(diào)用
innerLock.await();
innerLock.signalAll();
將靜態(tài)方法聲明為synchronized,調(diào)用該方法時會鎖住對應的類,此時其他線程無法調(diào)用該類的其他同步靜態(tài)方法
Tips:
- 內(nèi)部鎖不能中斷一個正在試圖獲得鎖的線程
- 鎖時不能設置超時
- 只有一個條件
該使用哪種鎖機制?
使用synchronized可減少代碼的編寫,減少出錯的幾率
使用ReentrantLock+Condition可以自行控制鎖的過程,實現(xiàn)多個條件
同步阻塞
使用其他對象的鎖來完成原子操作
public class bank{private Object lock = new Object();public void transfer(int from, int to, double money) {synchronized(lock){}}
}
監(jiān)視器
volatile
若如果只有一兩個域可能發(fā)生多線程的誤寫,可對該域聲明為volatile,虛擬機和編譯器就知道該域是可能被另一個線程并發(fā)更新的,但其不能保證原子性
boolean flag;
public void Not(){flag = !flag;
}
如上,不能保證其再讀取、翻轉(zhuǎn)和寫入時不被中斷
final和鎖
當把域聲明為final時,其他線程對其的讀取只能是構造成功后的值,而不會是null
fial Map<String, Double> accounts = new HashMap<>();
原子性
死鎖
線程局部變量
當多個線程都要調(diào)用Random中的方法生成隨機數(shù)時,由于Random是加鎖的,其他線程就得等待,此時可用TheadLocal輔助類為各個線程提供各自的Random實例
ThreadLocal<Random> threadLocal = ThreadLocal.withInitial(() -> new Random());
threadLocal.get().nextInt();
此外,專門創(chuàng)建多線程隨機數(shù)的ThreadLocalRandom,其current()方法會返回當前線程的Random類實例
ThreadLocalRandom.current().nextInt();
鎖超時
當線程調(diào)用tryLock()方法去申請另一個線程的鎖時,很有可能發(fā)生阻塞,故可在申請時設置時長
private ReentrantLock lock = new ReentrantLock();
try {lock.tryLock(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {e.printStackTrace();
}
讀寫鎖
讀寫鎖可從ReentrantReadWriteLock取出,為所有獲取方法加上讀鎖,為所有修改方法加上寫鎖
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock readLock = reentrantReadWriteLock.readLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
為什么棄用stop()和suspend()
stop()方法用來終止線程,并立即釋放線程所獲得的鎖,這會導致對象狀態(tài)不一致(如錢已被轉(zhuǎn)出,但在轉(zhuǎn)入前stop,會導致數(shù)據(jù)丟失)
故無法確定什么時候調(diào)用stop()是安全的,在希望停止線程的時候應該中斷線程,被中斷的線程會在安全的時候停止
suspend()方法用來阻塞線程,直至另一個線程調(diào)用resume(),當用suspend()掛起一個持有一個鎖的線程,則該鎖在resume()之前是不可用的。
若此時再用suspend()方法的線程獲取該鎖,則會死鎖,被掛起的鎖等待resume()釋放,而要resume()則要獲取被掛起的鎖