一、買票案例
1.1線程不安全方式
package com.learning.concurrent;
/**
* ClassName:TicketWindow
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:33
* @author:
*/
public class TicketWindow {
private int tickets;
public TicketWindow(int tickets) {
this.tickets = tickets;
}
public void sell (int sells) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets - sells >=0) {
tickets = tickets -sells;
}
}
public int getTickets() {
return tickets;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* ClassName:SellTicketTest
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:38
* @author:[email protected]
*/
@Slf4j
public class SellTicketTest {
public static void main(String[] args) throws Exception{
//模擬多人買票窗口
TicketWindow window=new TicketWindow(60000);
//線程集合
List<Thread> threadList = new ArrayList<Thread>();
//統計一共賣出了多少張票
Vector<Integer> sellCounts = new Vector<>();
long start = System.currentTimeMillis();
log.info("開始時間:{}",start);
for (int i=0 ;i<1000; i++) {
Thread t =new Thread(() ->{
window.sell(6);
sellCounts.add(6);
});
threadList.add(t);
t.start();
}
//等待所有買票線程跑完,再執行main線程中的方法
for ( Thread t: threadList) {
t.join();
}
long end = System.currentTimeMillis();
log.info("結束時間:{}",end);
log.info("耗時:{}",end - start);
log.info("賣出的票:{}",sellCounts.stream().mapToInt(i -> i).sum());
log.info("剩餘的票:{}",window.getTickets());
}
}
執行結果:
23:34:30.523 [main] INFO com.learning.concurrent.SellTicketTest - 開始時間:1589816070521
23:34:31.612 [main] INFO com.learning.concurrent.SellTicketTest - 結束時間:1589816071612
23:34:31.612 [main] INFO com.learning.concurrent.SellTicketTest - 耗時:1091
23:34:31.615 [main] INFO com.learning.concurrent.SellTicketTest - 賣出的票:6000
23:34:31.615 [main] INFO com.learning.concurrent.SellTicketTest - 剩餘的票:54582
線程不安全,因爲票數加起來不等於60000.
1.2線程安全方式
package com.learning.concurrent;
/**
* ClassName:TicketWindow
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:33
* @author:
*/
public class TicketWindow {
private int tickets;
public TicketWindow(int tickets) {
this.tickets = tickets;
}
public synchronized void sell (int sells) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets - sells >=0) {
tickets = tickets -sells;
}
}
public int getTickets() {
return tickets;
}
}
這裏在買票窗口中添加了synchronized ,並且休眠時間調成了10毫秒,執行結果如下:
23:38:35.179 [main] INFO com.learning.concurrent.SellTicketTest - 開始時間:1589816315178
23:38:45.940 [main] INFO com.learning.concurrent.SellTicketTest - 結束時間:1589816325940
23:38:45.940 [main] INFO com.learning.concurrent.SellTicketTest - 耗時:10762
23:38:45.944 [main] INFO com.learning.concurrent.SellTicketTest - 賣出的票:6000
23:38:45.944 [main] INFO com.learning.concurrent.SellTicketTest - 剩餘的票:54000
重要結論:
這個是線程安全的,因爲賣出的票+剩餘的票等於60000.但是觀察兩種結果,第一種結果我睡眠1000毫秒,大約花了1秒時間執行完成,第二種結果我睡眠了僅僅10毫秒,卻花了10s的時間執行完成,想象一下,當第1001個人來買票的時候,花了10s鍾才進行響應,多可怕,如果10000個人同時訪問呢?花費100s嗎?這個太可怕了。
二、轉賬案例
2.1線程不安全的方式
package com.learning.concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看轉賬2000次後的總金額
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 爲線程安全
static Random random = new Random();
// 隨機 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 賬戶
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);
}
}
}
輸出結果:
21:47:00.784 [main] DEBUG c.ExerciseTransfer - total:55
2.2線程安全的方式
上面的方式,共享變量有兩個,一個是A賬戶的餘額,一個是B賬戶的餘額,如果說單純的加synchronized關鍵字是不起作用的,因爲只能鎖住A對象或者B對象,而我們應該需要鎖住A和B賬戶共享的對象即可解決,即鎖住賬戶的class文件:
package com.learning.concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看轉賬2000次後的總金額
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 爲線程安全
static Random random = new Random();
// 隨機 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 賬戶
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) {
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
輸出結果:
21:47:42.392 [main] DEBUG c.ExerciseTransfer - total:2000
三、JAVA對象頭
也可以參考 https://blog.csdn.net/SCDN_CP/article/details/86491792
以32位的虛擬機爲例:
8個字節,其中4個字節是Mark Word。
普通對象:
數組對象:
其中Mark Word的結構爲:
64位虛擬機的Mark Word:
四、Monitor原理
Monitor 被翻譯爲監視器或管程
Monitor是操作系統提供的對象
每個 Java 對象都可以關聯一個 Monitor 對象,如果使用 synchronized 給對象上鎖(重量級)之後,該對象頭的Mark Word 中就被設置指向 Monitor 對象的指針
Monitor 結構如下:
- 剛開始 Monitor 中 Owner 爲 null
- 當 Thread-2 執行 synchronized(obj) 就會將 Monitor 的所有者 Owner 置爲 Thread-2,Monitor中只能有一
個 Owner - 在 Thread-2 上鎖的過程中,如果 Thread-3,Thread-4,Thread-5 也來執行 synchronized(obj),就會進入
EntryList BLOCKED - Thread-2 執行完同步代碼塊的內容,然後喚醒 EntryList 中等待的線程來競爭鎖,競爭的時是非公平的
- 圖中 WaitSet 中的 Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足進入 WAITING 狀態的線程,後面講
wait-notify 時會分析
注意:
synchronized 必須是進入同一個對象的 monitor 纔有上述的效果
不加 synchronized 的對象不會關聯監視器,不遵從以上規則