寫在前面
- 把技術概念通過文字的形式寫下來,理清邏輯,加深認知;
- 把知識點通過系列文章的形式分段寫下來,讓思維進行刻意的訓練;
- 把難懂的東西通過有趣的故事或者例子講出來,讓技術變得生動。
電影票的案例
單執行緒的例子
我們設定有一個電影院,該電影院開張不久,在入口的旁邊只設立了一個售票點A,顧客看電影,需要在售票點排隊依次買票,買完票後在入口處檢票進入電影院觀影。
上面的描述用程式碼來實現,可以是這樣的:
1、首先建立一個電影票的類:主要的屬性有票的ID,哪個放映廳,哪一排哪一列,放映的電影名稱,放映時間及票價。
/**
* 通過賣票程式讀懂多執行緒--電影票的類
*
* @author zhuhuix
* @date 2020-05-12
*/
public class Ticket {
//id
private int ticketId;
//放映廳
private String room;
//行
private Integer row;
//列
private Integer col;
//電影名
private String filmName;
//價格
private BigDecimal price;
//放映時間
private LocalDateTime datetime;
private Ticket(){
}
public Ticket(int ticketId,String room, Integer row, Integer col, String filmName, BigDecimal price, LocalDateTime datetime) {
this.ticketId = ticketId;
this.room = room;
this.row = row;
this.col = col;
this.filmName = filmName;
this.price = price;
this.datetime = datetime;
}
public int getTicketId() {
return ticketId;
}
public void setTicketId(int ticketId) {
this.ticketId = ticketId;
}
public String getRoom() {
return room;
}
public void setRoom(String room) {
this.room = room;
}
public Integer getRow() {
return row;
}
public void setRow(Integer row) {
this.row = row;
}
public Integer getCol() {
return col;
}
public void setCol(Integer col) {
this.col = col;
}
public String getFilmName() {
return filmName;
}
public void setFilmName(String filmName) {
this.filmName = filmName;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public LocalDateTime getDatetime() {
return datetime;
}
public void setDatetime(LocalDateTime datetime) {
this.datetime = datetime;
}
@Override
public String toString() {
return "Ticket{" +
"ticketId=" + ticketId +
", room='" + room + '\'' +
", row=" + row +
", col=" + col +
", filmName='" + filmName + '\'' +
", price=" + price +
", datetime=" + datetime +
'}';
}
}
2、其次建立一個顧客的類:主要的屬性有票的ID,購買的電影票;成員方法有買票。
/**
* 通過賣票程式讀懂多執行緒--顧客類
*
* @author zhuhuix
* @date 2020-05-12
*/
public class Customer {
//顧客id
private int customerId;
//購買的電影票
private Ticket ticket;
public Customer(int customerId) {
this.customerId = customerId;
}
//顧客買票
public void buyTicket(Ticket ticket) {
this.ticket = ticket;
}
public int getCustomerId() {
return customerId;
}
public void setCustomerId(int customerId) {
this.customerId = customerId;
}
public Ticket getTicket() {
return ticket;
}
public void setTicket(Ticket ticket) {
this.ticket = ticket;
}
@Override
public String toString() {
return "Customer{" +
"customerId=" + customerId +
", ticket=" + ticket.toString() +
'}';
}
}
3、最後寫一個主程式,生成電影票的列表,設定上門觀影的顧客人數,依次買票,輸出電影票購買的狀態。
/**
* 通過賣票程式讀懂多執行緒--單執行緒程式
*
* @author zhuhuix
* @date 2020-05-12
*/
public class TicketSingle {
private static final String ROOM = "中央放映廳";
private static final int ROW = 10;
private static final int COL = 20;
private static final String FILM_NAME = "戰狼3";
private static final BigDecimal PRICE = BigDecimal.valueOf(30);
private static List<Ticket> tickets = new ArrayList<>();
private static final int CUSTOMER_COUNT = 250;
private static List<Customer> customers = new ArrayList<>(CUSTOMER_COUNT);
public static void main(String[] args) {
//中央放映廳總共有250個座位,2020-05-12 18:00 放映戰狼3,售票價格為30元
int ticketId=1;
for (int row = 1; row <= ROW; row++) {
for (int col = 1; col <= COL; col++) {
Ticket ticket = new Ticket(ticketId++, ROOM, row, col,
FILM_NAME, PRICE,
LocalDateTime.of(2020, 5, 10, 18, 00));
tickets.add(ticket);
}
}
Iterator<Ticket> iterator = tickets.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().toString());
}
//顧客到售票點進行隨機買票
Collections.shuffle(tickets);
int index = 1;
while (tickets.size() > 0 && index <= CUSTOMER_COUNT) {
Ticket ticket = tickets.get(tickets.size() - 1);
Customer customer = new Customer(index);
customer.buyTicket(ticket);
customers.add(customer);
tickets.remove(ticket);
System.out.println(tickets.size() + "," + index);
System.out.println(index + "號顧客買到了"
+ "第" + customer.getTicket().getRow() + "行,第" + customer.getTicket().getCol() + "列的票");
index++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("電影票出售情況如下:");
//剩餘票的狀態
System.out.println("剩餘票數:" + tickets.size());
Iterator<Ticket> ticketIterator = tickets.iterator();
while (ticketIterator.hasNext()) {
System.out.println(ticketIterator.next().toString());
}
//顧客購買情況
System.out.println("買到票的人數:" + customers.size());
Iterator<Customer> customerIterator = customers.iterator();
while (customerIterator.hasNext()) {
System.out.println(customerIterator.next().toString());
}
System.out.println("未買到票的人數:" +(CUSTOMER_COUNT- customers.size()));
}
}
主程式的輸出情況是這樣的:
從單執行緒轉向多執行緒
一切井然有序,程式也執行得很好,那我們繼續往 下看,由於觀影顧客人數的增加,電影院對放映廳做了改造:1、增加座位;2、增設兩個賣票視窗。也就說原來只有一個視窗排隊單通道執行的程式變了,要允許原來的售票點與新增的售票點,同時進行賣票了。
有問題的多執行緒的例子
我們先簡單的在單執行緒的程式上做個多執行緒的改造:建立一個多執行緒的類,重寫run方法,將顧客買票的過程移至run方法中,在主程中設立”售票點A“,”售票點B“,”售票點C“三個執行緒讓其同時執行,對了,別忘了把ArrayList這個資料結構也改成Vector。改造後的程式是這樣的:
/**
* 通過賣票程式讀懂多執行緒--多執行緒
*
* @author zhuhuix
* @date 2020-05-12
*/
public class TicketThread extends Thread {
private static final String ROOM = "中央放映廳";
private static final int ROW = 20;
private static final int COL = 30;
private static final String FILM_NAME = "戰狼3";
private static final BigDecimal PRICE = BigDecimal.valueOf(30);
private static List<Ticket> tickets = new Vector<>();
private static final int CUSTOMER_COUNT = 800;
private static int customerId = 1;
private static List<Customer> customers = new Vector<>(CUSTOMER_COUNT);
TicketThread(String name) {
super(name);
}
@Override
public void run() {
while (tickets.size() > 0 && customerId <= CUSTOMER_COUNT) {
Ticket ticket = tickets.get(tickets.size() - 1);
ticket.setWindow(Thread.currentThread().getName());
Customer customer = new Customer(customerId);
customer.buyTicket(ticket);
customers.add(customer);
tickets.remove(ticket);
System.out.println(tickets.size() + "," + customerId);
System.out.println(Thread.currentThread().getName() + ":" + customerId + "號顧客買到了"
+ "第" + customer.getTicket().getRow() + "行,第" + customer.getTicket().getCol() + "列的票");
customerId++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//中央放映廳總共有250個座位,2020-05-12 18:00 放映戰狼3,售票價格為30元
int ticketId = 1;
for (int row = 1; row <= ROW; row++) {
for (int col = 1; col <= COL; col++) {
Ticket ticket = new Ticket(ticketId++, ROOM, row, col,
FILM_NAME, PRICE,
LocalDateTime.of(2020, 5, 10, 18, 00));
tickets.add(ticket);
}
}
Iterator<Ticket> iterator = tickets.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().toString());
}
//顧客到售票點進行隨機買票
Collections.shuffle(tickets);
TicketThread ticketThreadA = new TicketThread("售票點A");
TicketThread ticketThreadB = new TicketThread("售票點B");
TicketThread ticketThreadC = new TicketThread("售票點C");
ticketThreadA.start();
ticketThreadB.start();
ticketThreadC.start();
ticketThreadA.join();
ticketThreadB.join();
ticketThreadC.join();
System.out.println("電影票出售情況如下:");
//剩餘票的狀態
System.out.println("總共票數:" + ROW * COL + ",剩餘票數:" + tickets.size());
Iterator<Ticket> ticketIterator = tickets.iterator();
while (ticketIterator.hasNext()) {
System.out.println(ticketIterator.next().toString());
}
//顧客購買情況
System.out.println("買到票的人數:" + customers.size());
Iterator<Customer> customerIterator = customers.iterator();
while (customerIterator.hasNext()) {
System.out.println(customerIterator.next().toString());
}
System.out.println("未買到票的人數:" + (CUSTOMER_COUNT - customers.size()));
}
}
執行一下:總共只有600張票,買到票的人有614人?那進了電影院顧客肯定得投訴。
執行緒同步問題
我們分析一下:
電影票總共只有600張,三個視窗同時賣這600張票,電影票是個共享的池子,在多執行緒術語上稱為”共享資源“或”臨界資源“,每個執行緒訪問這些資源時,要保證”同步“:售票點A要賣第10排第9列的座位時,當且僅當同一時刻只有售票點A才能訪問這個座位對應的電影票,也就是所謂的不能一票多賣。
那多執行緒如何保證同步?通過加鎖!! 加鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源。
保證執行緒同步的例子
為了可以簡單地說明加鎖可以保證多執行緒同步,在下面的例子中僅實現對電影票共享池進行加鎖。
/**
* 通過賣票程式讀懂多執行緒--多執行緒
*
* @author zhuhuix
* @date 2020-05-12
*/
public class TicketThread extends Thread {
private static final String ROOM = "中央放映廳";
private static final int ROW = 20;
private static final int COL = 30;
private static final String FILM_NAME = "戰狼3";
private static final BigDecimal PRICE = BigDecimal.valueOf(30);
private volatile static List<Ticket> tickets = new Vector<>();
private static final int CUSTOMER_COUNT = 800;
private static int customerId = 1;
private volatile static List<Customer> customers = new Vector<>(CUSTOMER_COUNT);
TicketThread(String name) {
super(name);
}
@Override
public void run() {
while (tickets.size() > 0 && customerId <= CUSTOMER_COUNT) {
synchronized (TicketThread.class) {
//執行緒內兩次判斷,防止tickets 陣列溢位
if (tickets.size()>0) {
Ticket ticket = tickets.get(tickets.size() - 1);
ticket.setWindow(Thread.currentThread().getName());
Customer customer = new Customer(customerId);
customer.buyTicket(ticket);
customers.add(customer);
tickets.remove(ticket);
System.out.println(tickets.size() + "," + customerId);
System.out.println(Thread.currentThread().getName() + ":" + customerId + "號顧客買到了"
+ "第" + customer.getTicket().getRow() + "行,第" + customer.getTicket().getCol() + "列的票");
customerId++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
//中央放映廳總共有250個座位,2020-05-12 18:00 放映戰狼3,售票價格為30元
int ticketId = 1;
for (int row = 1; row <= ROW; row++) {
for (int col = 1; col <= COL; col++) {
Ticket ticket = new Ticket(ticketId++, ROOM, row, col,
FILM_NAME, PRICE,
LocalDateTime.of(2020, 5, 10, 18, 00));
tickets.add(ticket);
}
}
Iterator<Ticket> iterator = tickets.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().toString());
}
//顧客到售票點進行隨機買票
Collections.shuffle(tickets);
TicketThread ticketThreadA = new TicketThread("售票點A");
TicketThread ticketThreadB = new TicketThread("售票點B");
TicketThread ticketThreadC = new TicketThread("售票點C");
ticketThreadA.start();
ticketThreadB.start();
ticketThreadC.start();
ticketThreadA.join();
ticketThreadB.join();
ticketThreadC.join();
System.out.println("電影票出售情況如下:");
//剩餘票的狀態
System.out.println("總共票數:" + ROW * COL + ",剩餘票數:" + tickets.size());
Iterator<Ticket> ticketIterator = tickets.iterator();
while (ticketIterator.hasNext()) {
System.out.println(ticketIterator.next().toString());
}
//顧客購買情況
System.out.println("買到票的人數:" + customers.size());
Iterator<Customer> customerIterator = customers.iterator();
while (customerIterator.hasNext()) {
System.out.println(customerIterator.next().toString());
}
System.out.println("未買到票的人數:" + (CUSTOMER_COUNT - customers.size()));
}
}
執行情況如下:
票不超賣了:
每個視窗也實現了同步併發賣票:
同步的程式碼主要的改變來自於:
1、將賣票的過程用synchronized修飾,實現鎖的互斥,具體可以參考java多執行緒:synchronized的深度理解
synchronized (TicketThread.class) {
Ticket ticket = tickets.get(tickets.size() - 1);
ticket.setWindow(Thread.currentThread().getName());
Customer customer = new Customer(customerId);
customer.buyTicket(ticket);
customers.add(customer);
tickets.remove(ticket);
System.out.println(tickets.size() + "," + customerId);
System.out.println(Thread.currentThread().getName() + ":" + customerId + "號顧客買到了"
+ "第" + customer.getTicket().getRow() + "行,第" + customer.getTicket().getCol() + "列的票");
customerId++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2、將共享資源用volatile 修飾,實現執行緒訪問的視覺化,具體可以參考java多執行緒:volatile的深度理解
private volatile static List<Ticket> tickets = new Vector<>();
private volatile static List<Customer> customers = new Vector<>(CUSTOMER_COUNT);
寫在最後
程式所有的表達,歸根到底都是邏輯問題。而邏輯的核心是清晰高效地思考問題。對於多執行緒的理解,大家一定要起手來寫一些例程,融匯貫通才能體會到真諦,才能真正應用到工作實踐中去。