【十八掌●基本功篇】第一掌:Java之多線程--2-join、同步、死鎖、等待

這一篇博文是【大數據技術●降龍十八掌】系列文章的其中一篇,點擊查看目錄:這裏寫圖片描述大數據技術●降龍十八掌


系列文章:
【十八掌●基本功篇】第一掌:Java之IO
【十八掌●基本功篇】第一掌:Java之多線程–1-一些概念
【十八掌●基本功篇】第一掌:Java之多線程–2-join、同步、死鎖、等待

1、join() 方法

join()方法可以理解爲線程插隊。停止當前線程,先執行插入的線程,當插入的線程執行完畢後,再執行當前線程。看下面的例子:


package join;

/**
 * Created by 鳴宇淳 on 2017/12/7.
 */
public class MyJoinRunner implements Runnable {
    //子線程
    public void run() {
        for (int n = 0; n < 100; n++) {
            System.out.println(Thread.currentThread().getName() + ":" + n);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class MyJoin {
    public static void main(String[] args) throws InterruptedException {

        MyJoinRunner runner=new MyJoinRunner();

        Thread t1=new Thread(runner,"子線程");
        t1.start();

        //將子線程插隊,先執行子線程,然後再執行其他線程(主線程)
        t1.join();

        for (int n = 0; n < 100; n++) {
            System.out.println(Thread.currentThread().getName() + ":" + n);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

輸出結果:

子線程:0
子線程:1
子線程:2
子線程:3
子線程:4
子線程:5
子線程:6
.......
.......

main:0
main:1
main:2
main:3
main:4
main:5
.......
.......

2、一個多線程會出問題的例子

假設有個場景是自動賣票,一共有5張票,新建三個線程來同時賣票,往往就會出問題。如下實例所示。


/**
 * Created by 鳴宇淳 on 2017/12/8.
 */
public class SellTicketRunner implements Runnable {

    private int ticket = 5; //一共有n張票

    //一個子線程執行的方法
    public void run() {
        while (true) {
            if (this.ticket > 0) {
                //判斷如果票大於0,就先睡眠,以達到模擬的效果。
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //賣票
                this.ticket--;
                //打印剩餘票數
                System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);
            } else {
                break;
            }
        }
    }
}

public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner runner=new SellTicketRunner();
        System.out.println("賣票開始.....");

        Thread t1=new Thread(runner,"線程一");
        Thread t2=new Thread(runner,"線程二");
        Thread t3=new Thread(runner,"線程三");

        t1.start();
        t2.start();
        t3.start();        

        System.out.println("買票結束!");
    }
}

輸出爲:

線程一正在賣票;剩餘=4
線程二正在賣票;剩餘=2
線程三正在賣票;剩餘=2
線程一正在賣票;剩餘=1
線程三正在賣票;剩餘=-1
線程二正在賣票;剩餘=-1
線程一正在賣票;剩餘=-2
買票結束!

會發現,出現票超賣的情況,這是因爲在判斷餘票數量後、賣票操作前的階段,有可能有多個線程進入,然進行賣票操作。這就需要我們使用鎖和同步進行限制。

3、同步和鎖定

(1) 對象鎖

java中每個對象都有一個內置鎖。可以使用任意一個對象上的鎖,來實現線程的同步。看下面的實例,是將上面有問題的賣票代碼,添加上對象鎖,來控制線程同步,以解決超賣的問題。

package sellticket;

/**
 * Created by 鳴宇淳 on 2017/12/8.
 */
public class SellTicketRunner implements Runnable {

    private int ticket = 5; //一共有n張票

    private String lock=""; //創建一個對象,使用這個對象上的鎖來實現線程同步

    //一個子線程執行的方法
    public void run() {
        while (true) {
            synchronized (lock) {
                //將需要保護的代碼塊,鎖住,防止多個線程同時進入這個代碼塊
                if (this.ticket > 0) {
                    //判斷如果票大於0,就先睡眠,以達到模擬的效果。
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //賣票
                    this.ticket--;
                    //打印剩餘票數
                    System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);
                } else {
                    break;
                }
            }
        }
    }
}

public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner runner=new SellTicketRunner();

        Thread t1=new Thread(runner,"線程一");
        Thread t2=new Thread(runner,"線程二");
        Thread t3=new Thread(runner,"線程三");

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("買票結束!");
    }
}

(2) 方法鎖

可以在方法上添加上一個synchronized關鍵字,表明這個一個同步方法,通過鎖定一個方法,同時只讓一個線程執行這個方法,


package sellticket;

/**
 * Created by 鳴宇淳 on 2017/12/8.
 */
public class SellTicketRunner2 implements Runnable {
    private int ticket = 5; //一共有n張票

    public void run() {
        while (true) {
            boolean isQuit = sell();
            if (!isQuit) {
                break;
            }
        }
    }

    //在方法上添加一個synchronized關鍵字,表名是同步方法
    //將需要保護的方法,鎖住,防止多個線程同時進入這個方法
    private synchronized boolean sell() {
        System.out.println(Thread.currentThread().getName() + "進入");

        boolean isHav;
        if (this.ticket > 0) {
            isHav = true;
            //判斷如果票大於0,就先睡眠,以達到模擬的效果。
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //賣票
            this.ticket--;
            //打印剩餘票數
            System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);

        } else {
            isHav = false;
        }
        System.out.println(Thread.currentThread().getName() + "退出\n");
        return isHav;
    }
}


public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner2 runner=new SellTicketRunner2();

        System.out.println("賣票開始.....");

        Thread t1=new Thread(runner,"線程一");
        Thread t2=new Thread(runner,"線程二");
        Thread t3=new Thread(runner,"線程三");

        t1.start();
        t2.start();
        t3.start();
    }
}

當sell方法不加synchronized關鍵字時輸出,看上去售票過程比較亂:


賣票開始.....
線程二進入
線程一進入
線程三進入
線程三正在賣票;剩餘=3
線程一正在賣票;剩餘=3
線程二正在賣票;剩餘=3
線程一退出

線程一進入
線程三退出

線程二退出

線程二進入
線程三進入
線程二正在賣票;剩餘=2
線程二退出

線程二進入
線程三正在賣票;剩餘=2
線程三退出

線程三進入
線程一正在賣票;剩餘=2
線程一退出

線程一進入
線程一正在賣票;剩餘=1
線程一退出

線程一進入
線程一退出

線程三正在賣票;剩餘=-1
線程三退出

線程三進入
線程三退出

線程二正在賣票;剩餘=-1
線程二退出

線程二進入
線程二退出

如果加上synchronized,就能保證同時只有一個線程在執行sell方法,輸出爲:

賣票開始.....
線程一進入
線程一正在賣票;剩餘=4
線程一退出

線程一進入
線程一正在賣票;剩餘=3
線程一退出

線程一進入
線程一正在賣票;剩餘=2
線程一退出

線程一進入
線程一正在賣票;剩餘=1
線程一退出

線程一進入
線程一正在賣票;剩餘=0
線程一退出

線程一進入
線程一退出

線程二進入
線程二退出

線程三進入
線程三退出

(3) static 方法的同步

在非static方法上添加synchronized,相當於對當前類的對象this加鎖。

    private synchronized void fun() throws InterruptedException {       
        Thread.sleep(100);
    }

等同於:

    private void fun() throws InterruptedException {
        synchronized (this) {
            Thread.sleep(100);
        }
    }

但是在static方法是先於類的對象而存在的,當沒有實例化對象的時候,static方法已經可以調用了,那麼在static方法上添加synchronized ,是對什麼加鎖呢?其實是對類的class對象加鎖。

public class MyStaticDemo {
    private synchronized static void fun() throws InterruptedException {
        Thread.sleep(100);
    }
}

等同於:

public class MyStaticDemo {
    private static void fun() throws InterruptedException {
        synchronized (MyStaticDemo.class) {
            Thread.sleep(100);
        }
    }
}

4.死鎖

一個死鎖的實例:


/**
 * Created by 鳴宇淳 on 2017/12/11.
 */
public class DeadLockRunner implements Runnable {

    String[] source;
    String[] target;

    public DeadLockRunner(String[] source,String[] target)
    {
        this.source=source;
        this.target=target;
    }

    public void run() {
        synchronized (source) {
            System.out.println(Thread.currentThread().getName() + ":進入source到target拷貝");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (target) {
                for (int n = 0; n < source.length; n++) {
                    target[n] = source[0];
                }
            }

            System.out.println(Thread.currentThread().getName() + ":完成source到target拷貝");
        }
    }
}
public class DeadLock {

    public static void main(String[] args) {

        String[] source = new String[]{"a", "b", "c", "d"};
        String[] target = new String[]{"1", "2", "3", "4"};

        DeadLockRunner runner1 = new DeadLockRunner(source,target);
        DeadLockRunner runner2 = new DeadLockRunner(target,source);

        Thread t1=new Thread(runner1,"線程1");
        Thread t2=new Thread(runner2,"線程2");

        t1.start();
        t2.start();
    }
}

運行時可以發現輸出:

線程2:進入source到target拷貝
線程1:進入source到target拷貝

然後就卡在這裏不繼續運行了,根據輸出結果和分析代碼可得知,線程2是先執行的,線程2先鎖定source後,就sleep了,然後線程1進入後鎖定了target,也sleep了,當線程2醒來後要求target,但是被線程1鎖定了,線程1醒來要求source,但是被線程2鎖定了,兩個線程就造成了死鎖。

避免死鎖的方法:
要確定獲得鎖的順序,然後整個程序要遵守該順序,按相反的順序釋放鎖。

5. 線程等待

必須在同步環境內調用wait()、notify()、notfiyAll()方法,也就是說在在同步代碼塊或者同步方法內才能進行等待或者喚醒。wait()、notify()、notfiyAll()方法是Object的實例方法,所以每個對象上都可以有一個線程列表。

實例1:

package wait;

/**
 * Created by 鳴宇淳 on 2017/12/11.
 */
public class MyWaitRunner implements Runnable {

    private int total;

    public MyWaitRunner(int n) {
        this.total = n;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "進入執行");
        synchronized (this) {
            for (int i = 0; i < total; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //喚醒對象監視器上的單個線程,當前是喚醒線程1
                notify();
            }
        }
        System.out.println(Thread.currentThread().getName() + "執行完畢");
    }
}

public class MyWait {

    public static void main(String[] args) throws InterruptedException {

        MyWaitRunner runner1=new MyWaitRunner(100);
        Thread t1=new Thread(runner1,"線程1");
        t1.start();

        synchronized (t1)
        {
            System.out.println("主線程做一些事情......");
            System.out.println("等待子線程完成");
            //等待線程1完成
            t1.wait();
            System.out.println("子線程完成");
        }
    }
}

實例2:

下面這個例子是模擬一個場景,司機開車帶着乘客去北京遊覽故宮,乘客線程上車後,通知司機開車,然後乘客睡覺,讓司機在到達後叫醒自己,通過這個例子看一下兩個線程同步和通知功能的使用方法。



/**
 * Created by 鳴宇淳 on 2017/12/12.
 */
public class Passenger implements Runnable {

    //乘客線程方法
    public void run() {
        synchronized (ToBeiJing.lock) {
            System.out.println("[乘客]已經上車");
            try {
                //通知司機開車
                System.out.println("[乘客]通知司機開車");
                ToBeiJing.driverThread.start();

                System.out.println("[乘客]開始睡覺,到北京叫我");
                ToBeiJing.lock.wait();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("[乘客]醒了,開始遊覽故宮");
        }
    }
}

public class Driver implements Runnable {

    //司機線程方法
    public void run() {

        for (int n = 0; n < 5; n++) {
            System.out.println("[司機]正在開車");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("[司機]已經到北京了");

        synchronized (ToBeiJing.lock) {
            System.out.println("[司機]叫醒乘客");
            //叫醒乘客
            lock.notify();
        }
    }
}

public class ToBeiJing {

    //定義一個鎖,線程依據這個鎖實現同步
    public static String lock = "";
    //司機線程
    public static Thread driverThread;
    //乘客線程
    public static Thread passengerThread;

    public static void main(String[] args) throws InterruptedException {

        Driver driver = new Driver();
        Passenger passenger = new Passenger();

        driverThread = new Thread(driver, "司機線程");
        passengerThread = new Thread(passenger, "乘客線程");

        //乘客線程啓動
        passengerThread.start();
    }
}

第三個實例:

這是個經典的生產者消費者例子,有多個生產者和多個消費者同時在生產和消費,有一個倉庫,最大存儲量是固定的,所以在生產和消費的時候要收到倉庫量的限制,就需要到線程的同步和鎖。看以下代碼。


import java.util.ArrayList;
import java.util.List;

/**
 * Created by 鳴宇淳 on 2017/12/12.
 */
public class Demo {

    public static void main(String[] args) {

        //各個線程中,要生產或者消費的個數
        Integer[] producerNum = new Integer[]{10, 20, 30, 25, 40, 60};
        Integer[] consumerNum = new Integer[]{20, 25, 35, 15, 50};

        Godown godown = new Godown();
        List<Thread> threads = new ArrayList<Thread>();

        //創建生產者線程
        for (Integer p : producerNum) {
            Thread t = new Thread(new Producer(p, godown));
            threads.add(t);
        }

        //創建消費者線程
        for (Integer c : consumerNum) {
            Thread t = new Thread(new Consumer(c, godown));
            threads.add(t);
        }

        System.out.println("當前倉庫中數量:" + godown.currNum);

        //啓動生產者和消費者線程
        for (Thread t : threads) {
            t.start();
        }
    }
}


//倉庫類
public class Godown {

    //最大庫存量
    public static final int MAX_SIZE = 100;

    public int currNum;//當前庫存量

    /*
    生產方法
     */
    public synchronized void produce(int addNum) throws InterruptedException {
        while (addNum + currNum > MAX_SIZE) {
            System.out.println("要生產" + addNum + ",倉庫空位數爲" + (MAX_SIZE - currNum) + ",暫停生產");
            this.wait();
        }

        //可以生產
        currNum = currNum + addNum;
        System.out.println("生產了" + addNum + ",當前庫存量爲:" + currNum);
        //喚醒
        this.notifyAll();
    }

    public synchronized void consume(int needNum) throws InterruptedException {
        while (currNum < needNum) {
            System.out.println("要求消費" + needNum + "個,剩餘" + currNum + "個,不能消費,等待生產");
            this.wait();
        }
        currNum = currNum - needNum;
        System.out.println("消費了" + needNum + "個,當前庫存量爲:" + currNum);

        //喚醒
        notifyAll();
    }
}

/**
 * Created by 鳴宇淳 on 2017/12/12.
 * <p>
 * 生產者類
 */
public class Producer implements Runnable {

    private int addNum;
    private Godown godown;

    public Producer(int addNum, Godown godown) {
        this.addNum = addNum;
        this.godown = godown;
    }

    public void run() {
        try {
            this.godown.produce(this.addNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * Created by 鳴宇淳 on 2017/12/12.
 * 消費者
 */
public class Consumer implements Runnable {
    private int needNum;
    private Godown godown;

    public Consumer(int needNum, Godown godown) {
        this.needNum = needNum;
        this.godown = godown;
    }

    public void run() {
        try {
            this.godown.consume(needNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章