執行緒的同步

南煎丸子發表於2020-07-21

執行緒的同步

執行緒的安全問題

  • 多個執行緒執行的不確定性引起執行的結果的不穩定性
  • 多個執行緒對資料的共享,會造成操作的不完整性、會破壞資料(例如視窗買票問題,多個視窗對票數進行共享,會出現兩個視窗賣號碼相同的票給不同的人)

通過同步機制解決執行緒安全問題

方法一:同步程式碼塊

格式

synchronized(同步監視器){

    需要被同步的程式碼

    }

舉例說明

class Thread implements Runnable{

    private Object obj = new Object();

    public void run() {
        //使用類物件充當鎖
        synchronized(obj){
        .......
        }
    }
}

說明

  • 操作共享資料的程式碼即為需要被同步的程式碼
    • 不能多包含程式碼,也不能少包含程式碼
  • 共享資料:多個執行緒共同操作的變數
  • 同步監視器:俗稱鎖
    • 任何一個類的物件都可以來充當鎖
    • 要求多個執行緒必須共用同一把鎖
    • 在實現Runnable介面建立多執行緒的方式中,考慮使用this充當同步監視器
    • 在繼承Thread類建立多執行緒的方式中,慎用this來充當同步監視器,考慮使用當前類來充當同步監視器

特點

  • 好處:解決執行緒的安全問題
  • 侷限性:操作同步程式碼時,只能有一個執行緒參與,其他執行緒等待。相當於一個單執行緒的過程,效率低

程式碼實現

實現Runnable介面建立多執行緒的方式
/**
 * 建立三個視窗買票,票數100張:使用實現Runnable介面的方式實現的
 */
class WindowThread implements Runnable{

    private int ticket = 100;
    // private Object obj = new Object();

    public void run() {
        while (true) {
            //此時this:唯一的WindowThread物件
            synchronized(this){// 方式二:synchronized(obj){
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread(). getName() + ":" + "買票,票號為" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

public class Test1 {

    public static void main(String[] args) {
        WindowThread window = new WindowThread();

        Thread w1 = new Thread(window);
        Thread w2 = new Thread(window);
        Thread w3 = new Thread(window);

        w1.setName("視窗1");
        w1.start();
        w2.setName("視窗2");
        w2.start();
        w3.setName("視窗3");
        w3.start();
    }
}
繼承Thread類建立多執行緒的方式
class Window extends Thread {
    // 大家公用資料,只有100張票
    private static int ticket = 100;
    private static Object obj = new Object();
    public void run() {
        while (true) {
            //方式二
            synchronized(Window.class){
                // 方式一:synchronized(obj){
                //synchronized(this)錯誤的,此時this代表著三個物件
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":" + "買票,票號為" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }

        }
    }
}

public class Test2 {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("視窗1");
        w2.setName("視窗2");
        w3.setName("視窗3");

        w1.start();
        w2.start();
        w3.start();
    }
}

同步方法

如果操作共享資料的程式碼完整的宣告在一個方法中,就可以將此方法宣告同步的

格式

利用synchronized 修飾方法

public synchronized void XXX(){

}

public static synchronized void XXX(){

}

說明

  • synchronized修飾方法時鎖定的是呼叫該方法的物件
  • 同步方法仍然涉及到同步監視器,只是不需要我們顯示的宣告
  • 非靜態的同步方法,同步監視器是this
  • 靜態的同步方法,同步監視器是當前類本身(Window.class)

程式碼實現

實現Runnable介面建立多執行緒的方式

非靜態同步方法,呼叫this

class WindowThread3 implements Runnable{

    private int ticket = 100;
    private static boolean isFlag = true;
    // private Object obj = new Object();

    public void run() {
        while (isFlag) {
            show();
        }
    }

    public synchronized void show(){//同步監視器:this
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread(). getName() + ":" + "買票,票號為" + ticket);
            ticket--;
        }else{
            isFlag = false;
        }
    }
}

public class Test3 {
    public static void main(String[] args) {
        WindowThread3 window = new WindowThread3();

        Thread w1 = new Thread(window);
        Thread w2 = new Thread(window);
        Thread w3 = new Thread(window);

        w1.setName("視窗1");
        w1.start();
        w2.setName("視窗2");
        w2.start();
        w3.setName("視窗3");
        w3.start();
    }
}
繼承Thread類建立多執行緒的方式

靜態同步方法,呼叫當前類本身

class Window4 extends Thread{
    private static int ticket = 100;
    private static boolean isFlag = true;

    @Override
    public void run() {
        while(isFlag){
            show();
        }
    }

    public static synchronized void show(){
        //同步監視器:Window.class
        if(ticket > 0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread(). getName() + ":" + "買票,票號為" + ticket);
            ticket--;
        }else{
            isFlag = false;
        }
    }
}

public class Test4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();

        w1.setName("視窗1");
        w2.setName("視窗2");
        w3.setName("視窗3");

        w1.start();
        w2.start();
        w3.start();
    }
}

通過Lock(鎖)解決執行緒安全問題

步驟

  1. 例項化ReentrantLock

    private ReentrantLock lock = new ReentrantLock(true);

    • true代表公平
    • 不填預設為false
  2. 呼叫鎖的方法

    在可能會出現安全問題程式碼前呼叫Lock介面中的方法Lock獲取鎖
    lock.lock();

  3. 呼叫解鎖的方法

    lock.unlock();

注意:其中呼叫lock()方法和unlock()方法時要用try()finally()包住

程式碼實現

class Window5 implements Runnable {

    private int ticket = 100;

    //1.例項化ReentrantLock
    private ReentrantLock lock = new ReentrantLock(true);

    public void run() {
        while (true) {
            try{
                //2.呼叫鎖定的方法:lock()
                lock.lock();

                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() +  "賣票" + ":" + "票號為" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally{
                //3.呼叫解鎖的方法:unlock()
                lock.unlock();
            }
        }

    }

}

public class Test5 {
    public static void main(String[] args) {
        Window5 window = new Window5();

        Thread t1 = new Thread(window);
        Thread t2 = new Thread(window);
        Thread t3 = new Thread(window);

        t1.setName("視窗1:");
        t2.setName("視窗2:");
        t3.setName("視窗3:");

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

synchronized和Lock的異同

  • synchronized機制在執行完相應的程式碼邏輯後自動釋放同步監視器
  • Lock需要手動的啟動同步(lock),同時結束同步也需要手動的實現(unlock)

  • 都可以解決執行緒安全問題

釋放鎖與不釋放鎖的操作

釋放鎖的操作

  • 當前執行緒的同步方法、同步程式碼塊執行結束
  • 當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束
  • 當前執行緒在同步程式碼塊、同步方法中遇到了break、return終止了該程式碼塊、方法的繼續執行
  • 當前執行緒在同步程式碼塊、同步方法中執行了執行緒物件的wait()方法,當前執行緒暫停,並釋放鎖

不釋放鎖的操作

  • 執行緒在執行同步程式碼塊或同步方法時,程式呼叫了Thread.sleep()或Thread.yield()方法暫停當前執行緒的執行
  • 執行緒在執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放鎖(同步監視器)
    • 儘量避免使用suspend()(掛起)和resume()(繼續執行)來控制執行緒

使用順序

Lock--->同步程式碼塊--->同步方法

死鎖

  • 不同執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
  • 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續
  • 使用同步時,避免出現死鎖
  • 避免
    • 專門的演算法
    • 儘量減少同步資源的定義
    • 儘量避免巢狀同步

相關文章