Java多線程(三)同步與鎖定

轉載請註明出處:http://blog.csdn.net/github_39430101/article/details/77488972
在併發編程中發生的最常見的一種情況是超過一個執行線程使用共享資源。

Java內存模型與多線程

Jvm有主內存(Main Memory)和工作內存,主內存其實就是我們平時說的Java堆內存,存放程序中所有的類實例、靜態數據等變量,是多個線程共享的,而工作內存存放的是該線程從主內存中拷貝過來的變量以及訪問方法所取得的局部變量,是每個線程私有的其他線程不能訪問,每個線程對變量的操作都是以先從主內存將其拷貝到工作內存再對其進行操作的方式進行,多個線程之間不能直接互相傳遞數據通信,只能通過共享變量來進行。

這裏寫圖片描述

同步

隱式鎖synchronized,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多隻有一個線程執行該代碼。synchronized修飾地方只有兩個:

  • 一是在方法聲明時使用,放在範圍操作符(public等)之後,返回類型聲明(void等)之前的方法名上面,代碼如下:
public synchronized void synMethod(){ 
    .....
}
  • 二是修飾在代碼塊上面的,對某一代碼塊使用synchronized(Object),指定加鎖對象:
public int synMethod(int a){
    synchronized(this){
        .....
    }
}

示例

public class Web12306 implements Runnable {
    private int num = 10;
    @Override
    public void run() {
        synchronized(this){  
           while(true){
            if(num<=0){
                break; //跳出循環
            }      
           System.out.println(Thread.currentThread().getName()+"搶到了"+num--); 
        }  
      }
  }

    public static void main(String[] args) {
        Web12306 web = new Web12306();
        Thread t1 = new Thread(web,"路人甲");
        Thread t2 = new Thread(web,"黃牛乙");
        Thread t3 = new Thread(web,"攻城獅");
        t1.start();
        t2.start();
        t3.start();
    }

}
//輸出:
路人甲搶到了10
路人甲搶到了9
路人甲搶到了8
路人甲搶到了7
路人甲搶到了6
路人甲搶到了5
路人甲搶到了4
路人甲搶到了3
路人甲搶到了2
路人甲搶到了1

死鎖

過多的同步容易造成死鎖,這裏我們舉一個一手給錢一手給貨的例子

public class Demo{
    public static void main(String[] args) {
        Object g = new Object();
        Object m = new Object();
        Test t1 = new Test(g,m);
        Test2 t2 = new Test2(g,m);
        Thread proxy = new Thread(t1);
        Thread proxy2 = new Thread(t2);
        proxy.start();
        proxy2.start();
    }
}
class Test implements Runnable{
    public Test(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }
    Object goods;
    Object money;

    @Override
    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized(goods){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(money){

            }
        }
        System.out.println("一手給錢");
    }
}
class Test2 implements Runnable{
    Object goods ;
    public Test2(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }
    Object money ;

    @Override
    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized(money){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(goods){

            }
        }
        System.out.println("一手給貨");
    }
}

生產者消費者模式(用來平衡生產者和消費者的處理能力)

生產者消費者問題是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的線程–即所謂的生產者和消費者在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。於此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區慢時加入數據,消費者也不會在緩衝區中空時消耗數據。
要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,載喚醒消費者。通常常用的方法有信號燈法、管程等。如果解決方法不夠完成,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。
這裏寫圖片描述
信號燈法:
創建一個Movie類

public class Movie {
    private String pic;
    /*
     * 信號燈 flag -->true 生產者生產,消費者等待,生產完成後通知消費 flag -->false
     * 消費者消費,生產者等待,消費完成後通知生產
     */
    private boolean flag = true;

    public synchronized void play(String pic) {
        if (!flag) { // 等待
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //開始生產
        try {
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("生產了"+pic);
        //生產完畢
        this.pic = pic;
        //通知消費者
        this.notify();
        //生產者停下
        this.flag = false;
    }

    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //開始消費
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("消費了"+pic);
        //消費完畢
        //通知生產
        this.notifyAll();
        this.flag = true;
    }
}

生產者電影工廠

public class Player implements Runnable {
    private Movie m;
    public Player(Movie m) {
        super();
        this.m = m;
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(0==i%2){
                m.play("飛躍瘋人院");
            }else 
                m.play("肖申克的救贖");
        }
    }

}

消費者

public class Watcher implements Runnable {
    private Movie m;
    public Watcher(Movie m) {
        super();
        this.m = m;
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            m.watch();
        }
    }

}

主線程

public class App {
    public static void main(String[] args) {
        Movie m = new Movie();
        Player p = new Player(m);
        Watcher w = new Watcher(m);
        new Thread(p).start();
        new Thread(w).start();
    }
}

運行結果:
這裏寫圖片描述

任務調度

  • Timer定時器類
  • TimerTask任務類
  • 通過java timer timertask:(Spring的任務調度就是通過他們來實現的)
  • 在這種實現方式中,Timer類實現的是類似鬧鐘的功能,也就是定時或者每隔一定時間觸發一次線程。其實,Timer類本身實現的就是一個線程,只是這個線程是用來實現調用其他線程的。而TimerTask類是一個抽象類,該類實現了Runnable接口,所以按照前面的介紹,該類具備多線程的能力
  • 在這種實現方式中,通過繼承TimerTask使該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啓動線程的執行。
  • 在實際使用時,一個Timer可以啓動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間如果需要完全獨立運行的話,最好還是一個Timer啓動一個TimerTask實現。
public class TimeDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run(){
                System.out.println("so easy....");
            }
        }, new Date(System.currentTimeMillis()+1000), 200);
    }
}

運行結果:
這裏寫圖片描述

總結

1、sleep()

使當前線程(即調用該方法的線程)暫停執行一段時間,讓其他線程有機會繼續執行,但它並不釋放對象鎖。也就是說如果有synchronized同步快,其他線程仍然不能訪問共享數據。注意該方法要捕捉異常。

2、join()

join()方法使調用該方法的線程在此之前執行完畢,也就是等待該方法的線程執行完畢後再往下繼續執行。注意該方法也需要捕捉異常。

3、yield()

該方法與sleep()類似,只是不能由用戶指定暫停多長時間,並且yield()方法只能讓同優先級的線程有執行的機會。yield方法使當前線程讓出CPU佔有權,但讓出的時間是不可設定的。不會釋放鎖標誌。

4、wait()和notify()、notifyAll()

這三個方法用於協調多個線程對共享數據的存取,所以必須在synchronized語句塊內使用。synchronized關鍵字用於保護共享數據,阻止其他線程對共享數據的存取,但是這樣程序的流程就很不靈活了,如何才能在當前線程還沒退出synchronized數據塊時讓其他線程也有機會訪問共享數據呢?此時就用這三個方法來靈活控制。

wait()方法使當前線程暫停執行並釋放對象鎖標示,讓其他線程可以進入synchronized數據塊,當前線程被放入對象等待池中。當調用notify()方法後,將從對象的等待池中移走一個任意的線程並放到鎖標誌等待池中,只有鎖標誌等待池中線程能夠獲取鎖標誌;如果鎖標誌等待池中沒有線程,則notify()不起作用。
notifyAll()則從對象等待池中移走所有等待那個對象的線程並放到鎖標誌等待池中。

5、關鍵字synchronized
該關鍵字用於保護共享數據,當然前提條件是要分清哪些數據是共享數據。每個對象都有一個鎖標誌,當一個線程訪問到該對象,被Synchronized修飾的數據將被”上鎖”,阻止其他線程訪問。當前線程訪問完這部分數據後釋放鎖標誌,其他線程就可以訪問了。

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