【JAVA併發編程-黑馬】第二章

一、買票案例

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 的對象不會關聯監視器,不遵從以上規則

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章