1. 程式人生 > >Java學習多執行緒第二天

Java學習多執行緒第二天

內容介紹

  •  執行緒安全
  •  執行緒同步
  •  死鎖
  •  Lock鎖
  •  等待喚醒機制

1    多執行緒

1.1     執行緒安全

如果有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。程式每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

 我們通過一個案例,演示執行緒的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。

我們來模擬電影院的售票視窗,實現多個視窗同時賣 “魔童哪吒”這場電影票(多個視窗一起賣這100張票)

需要視窗,採用執行緒物件來模擬;需要票,Runnable介面子類來模擬

  •  測試類
public class ThreadDemo {
    public static void main(String[] args) {
        //建立票物件
        Ticket ticket = new Ticket();
        
        //建立3個視窗
        Thread t1  = new Thread(ticket, "視窗1");
        Thread t2  = new Thread(ticket, "視窗2");
        Thread t3  = new Thread(ticket, "視窗3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}
  •   模擬票

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    @Override
    public void run() {
        //模擬賣票
        while(true){
            if (ticket > 0) {
                //模擬選坐的操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }
} 

執行結果發現:上面程式出現了問題

  •   票出現了重複的票
  •   錯誤的票 0、-1

其實,執行緒安全問題都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

1.2     執行緒同步(執行緒安全處理Synchronized)

java中提供了執行緒同步機制,它能夠解決上述的執行緒安全問題。

         執行緒同步的方式有兩種:

  •   方式1:同步程式碼塊
  •   方式2:同步方法

1.2.1    同步程式碼塊

同步程式碼塊: 在程式碼塊宣告上 加上synchronized

synchronized (鎖物件) {
    可能會產生執行緒安全問題的程式碼
}

同步程式碼塊中的鎖物件可以是任意的物件;但多個執行緒時,要使用同一個鎖物件才能夠保證執行緒安全。

使用同步程式碼塊,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖物件
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步程式碼塊
            synchronized (lock){
                if (ticket > 0) {
                    //模擬電影選坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}

當使用了同步程式碼塊後,上述的執行緒的安全問題,解決了。

1.2.3    同步方法

  •   同步方法:在方法宣告上加上synchronized
public synchronized void method(){
       可能會產生執行緒安全問題的程式碼
}

同步方法中的鎖物件是 this

使用同步方法,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖物件
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步方法
            method();
        }
    }

//同步方法,鎖物件this
    public synchronized void method(){
        if (ticket > 0) {
            //模擬選坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
}
  •   靜態同步方法: 在方法宣告上加上static synchronized
public static synchronized void method(){
可能會產生執行緒安全問題的程式碼
}

靜態同步方法中的鎖物件是 類名.class

1.3     死鎖

同步鎖使用的弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){
    synchronized(B鎖){
         
}
}

我們進行下死鎖情況的程式碼演示:

  •   定義鎖物件類
public class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}
  •   執行緒任務類
public class ThreadTask implements Runnable {
    int x = new Random().nextInt(1);//0,1
    //指定執行緒要執行的任務程式碼
    @Override
    public void run() {
        while(true){
            if (x%2 ==0) {
                //情況一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if大口吃肉");
                    }
                }
            } else {
                //情況二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else大口吃肉");
                    }
                }
            }
            x++;
        }
    }
}
  •   測試類
public class ThreadDemo {
    public static void main(String[] args) {
        //建立執行緒任務類物件
        ThreadTask task = new ThreadTask();
        //建立兩個執行緒
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        //啟動執行緒
        t1.start();
        t2.start();
    }
}

1.4     Lock介面

查閱API,查閱Lock介面描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。

Lock提供了一個更加面對物件的鎖,在該鎖中提供了更多的操作鎖的功能。

我們使用Lock介面,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    
    //建立Lock鎖物件
    Lock ck = new ReentrantLock();
    
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //synchronized (lock){
            ck.lock();
                if (ticket > 0) {
                    //模擬選坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            ck.unlock();
            //}
        }
    }
}

1.5     等待喚醒機制

在開始講解等待喚醒機制之前,有必要搞清一個概念——執行緒之間的通訊:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。通過一定的手段使各個執行緒能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

  •   wait() :等待,將正在執行的執行緒釋放其執行資格 和 執行權,並存儲到執行緒池中。
  •   notify():喚醒,喚醒執行緒池中被wait()的執行緒,一次喚醒一個,而且是任意的。
  •   notifyAll(): 喚醒全部:可以將執行緒池中的所有wait() 執行緒都喚醒。

其實,所謂喚醒的意思就是讓 執行緒池中的執行緒具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的執行緒。

仔細檢視JavaAPI之後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable介面中,卻被定義在了Object類中,為什麼這些操作執行緒的方法定義在Object類中?

因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意物件。能被任意物件呼叫的方法一定定義在Object類中。

接下里,我們先從一個簡單的示例入手:

如上圖說示,輸入執行緒向Resource中輸入name ,sex , 輸出執行緒從資源中輸出,先要完成的任務是:

  •   1.當input發現Resource中沒有資料時,開始輸入,輸入完成後,叫output來輸出。如果發現有資料,就wait();
  •   2.當output發現Resource中沒有資料時,就wait() ;當發現有資料時,就輸出,然後,叫醒input來輸入資料。

下面程式碼,模擬等待喚醒機制的實現:

  •  模擬資源類
public class Resource {
    private String name;
    private String sex;
    private boolean flag = false;

    public synchronized void set(String name, String sex) {
        if (flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // 設定成員變數
        this.name = name;
        this.sex = sex;
        // 設定之後,Resource中有值,將標記該為 true ,
        flag = true;
        // 喚醒output
        this.notify();
    }

    public synchronized void out() {
        if (!flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // 輸出執行緒將資料輸出
        System.out.println("姓名: " + name + ",性別: " + sex);
        // 改變標記,以便輸入執行緒輸入資料
        flag = false;
        // 喚醒input,進行資料輸入
        this.notify();
    }
}
  • 輸入執行緒任務類
public class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            if (count == 0) {
                r.set("小明", "男生");
            } else {
                r.set("小花", "女生");
            }
            // 在兩個資料之間進行切換
            count = (count + 1) % 2;
        }
    }
}
  • 輸出執行緒任務類
public class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}
  • 測試類
public class ResourceDemo {
    public static void main(String[] args) {
        // 資源物件
        Resource r = new Resource();
        // 任務物件
        Input in = new Input(r);
        Output out = new Output(r);
        // 執行緒物件
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        // 開啟執行緒
        t1.start();
        t2.start();
    }
}

&n