兩個經典的執行緒安全示例分析
阿新 • • 發佈:2020-12-18
示例1:賣票與買票
@Slf4j(topic = "c.CASE1")
public class CASE1 {
//隨機數
public static Random random = new Random();
public static int getRandom(){
return random.nextInt(5) + 1;
}
public static void main(String[] args) {
//開始有2000張票
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
//用來儲存賣出去多少張票,//競態條件
//List<Integer> sellCount = new ArrayList<>();
List<Integer> sellCount = new Vector<>();
//2000票分2000次賣出去
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(()->{
//競態態條件
int sell = ticketWindow.sell(getRandom());
sellCount.add(sell);
});
list.add(t);
t.start();
}
//使得每個執行緒執行結束
list.forEach((t)->{
try {
t. join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//統計最終結果
log.debug("賣出去:" + sellCount.stream().mapToInt(a->a).sum());
log.debug("剩餘票數:" + ticketWindow.getCount());
}
}
class TicketWindow{
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
//賣出去多少張表,如果返回0則表示沒賣出去票
public int sell(int amount){
if(amount < count){
count = count - amount;
return amount;
}else{
return 0;
}
}
}
請判斷該例子是否存線上程安全?
執行輸出結果:
答:存在!
分析:判斷是否存線上程安全步驟
-
尋找臨界區 :
int sell = ticketWindow.sell(getRandom()); sellCount.add(sell);
-
尋找共享變數
兩個共享變數count,sellCount,存線上程安全的只有count變數,因為sellCount的add方法是屬於執行緒安全的,如果換成ArrayList就會出現執行緒安全問題,count變數屬於ticketWindow物件。
解決方法:
可以給共享變數上鎖,使用關鍵字synchronized
,可以將鎖加在sell方法上
public synchronized int sell(int amount){
if(amount < count){
count = count - amount;
return amount;
}else{
return 0;
}
}
等價於
public int sell(int amount){
synchronized(this){
if(amount < count){
count = count - amount;
return amount;
}else{
return 0;
}
}
}
示例2:ab互相轉賬
@Slf4j(topic = "c.CASE2")
public class CASE2 {
private static Random random = new Random();
//返回1-100隨機數
public static int getRandom(){
return random.nextInt(100) + 1;
}
public static void main(String[] args) throws InterruptedException {
//賬戶a
Account a = new Account(1000);
//賬戶b
Account b = new Account(1000);
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
a.transfer(b,getRandom());
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
b.transfer(a,getRandom());
}
});
//分別啟動t1 t2
t1.start();
t2.start();
//等待t1 t2跑完
t1.join();
t2.join();
log.debug("賬戶a :" + a.getMoney() );
log.debug("賬戶b :" + b.getMoney());
log.debug("total:{}",(a.getMoney() +b.getMoney()));
}
}
class Account{
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void transfer(Account target,int amount){
//有錢可轉
if(this.money > amount){
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
執行結果可以看出來,存線上程安全問題!
分析:
-
臨界區
public void transfer(Account target,int amount){ //有錢可轉 synchronized (Account.class){ if(this.money > amount){ this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } }
-
共享變數
與上一個例子不同的是,該臨界區存在的共享變數有兩個,分別是
this.money
和target.money
,所以如果在transfer上面加上鎖,還能不能鎖得住,能 不能解決執行緒安全問題呢?public synchronized void transfer(Account target,int amount){ //有錢可轉 if(this.money > amount){ this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } }
並不能!
原因是:此時的synchronized只能鎖定住當前的物件的共享變數,也就是說誰呼叫transfer就會鎖住誰的money屬性,因此並不能解決執行緒安全問題,那麼該如果才能同時鎖住兩個物件呢?
解決方法:擴大鎖的範圍,從鎖住物件擴大至鎖住整個類,因為無論是this.money
和target.money
它們都屬於Account類,因此可以修改臨界區的鎖範圍,如下:
public void transfer(Account target,int amount){
//有錢可轉
synchronized (Account.class){
if(this.money > amount){
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
重新執行程式碼:
此時便達到我們的執行緒安全目的。