Java多線程02(線程安全、線程同步、等待喚醒機制)

Java多線程2(線程安全、線程同步、等待喚醒機制、單例設計模式)

一、線程安全

  • 若是有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。
    • 經過案例演示線程的安全問題:電影院要賣票。
    • 咱們模擬電影院的賣票過程。假設本場電影的座位共100個(本場電影只能賣100張票)。
    • 咱們來模擬電影院的售票窗口,實現多個窗口同時賣這場電影的票(多個窗口一塊兒賣這100張票)
    • 須要窗口,採用線程對象來模擬;
    • 須要票,Runnable接口子類來模擬;
  • 代碼:
public class Tickets implements Runnable {
    private int num = 100;  //(1)
    @Override
    public void run() {  // (2)
        // 死循環,一直處於能夠售票狀態
        while(true) {  // (3)
            if(num>0) {  // (4)
                System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  張票售出");  //(5)
            }
        }
    }
}

public class TicketsDemo {
    public static void main(String[] args) {  //(6)
        Tickets t = new Tickets();  // (7)
        new Thread(t).start();  // (8)
        new Thread(t).start();  // (9)
        new Thread(t).start();  // (10)
    }
}
  • 分析:
    • 三個窗口每一個窗口都在買票,假設此時只剩一張票,可能會發生如下狀況:
    • 線程t1執行run方法到(4)時,產生阻塞,線程t2執行run方法到(4)時葉阻塞,線程t3執行完了run方法,釋放CPU,此時num=0;t1再次獲得CPU時,不會再次判斷,而是直接執行下一步(5),這時就會發生0--,出現出售第0張票,而且票數變成負數,這樣就出現了安全隱患。
  • 運行結果發現:上面程序出現了問題
    • 票出現了重複的票
    • 錯誤的票 0、-1
  • 線程安全問題都是由全局變量及靜態變量引發的。
  • 若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。java

  • 解決辦法:
    • 當一個線程進入數據操做的時候,不管是否休眠,其餘線程智能等待。

二、線程同步(線程安全處理Synchronized)

  • java中提供了線程同步機制,它可以解決上述的線程安全問題。
  • 線程同步的方式有兩種:
    • 方式1:同步代碼塊
    • 方式2:同步方法

2.1 同步代碼塊

  • 同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {
    可能會產生線程安全問題的代碼
}
  • 同步代碼塊中的鎖對象能夠是任意的對象;但多個線程時,要使用同一個鎖對象纔可以保證線程安全。設計模式

  • 使用同步代碼塊,對電影院賣票案例中Ticket類進行以下代碼修改:安全

/*
    經過線程休眠,出現安全問題
    解決安全問題,Java程序,提供同步技術
    公式:
        syncronized (任意對象){
            線程要操做的共享數據
        }
*/
public class Tickets implements Runnable {
    // 定義出售的票數
    private int num = 100;
    Object obj = new Object();  // 建立對象,用於同步
    @Override
    public void run() {
        // 死循環,一直處於能夠售票狀態
        while(true) {
            // 線程共享數據,保證安全,加入同步代碼塊
            synchronized (obj) {
                if(num>0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  張票售出");
                }
            }
        }
    }
}
  • 當使用了同步代碼塊後,上述的線程的安全問題,解決了。
  • 分析:
    • 同步對象:能夠是任意對象,能夠稱之爲同步鎖,對象監視器,注意不能用匿名內部類,由於這樣在會致使每次得到鎖對象都是新的對象,沒法實現加鎖的效果。
    • 同步是如何保證安全性的:沒有鎖的線程不能執行,只能等待。
    • 具體執行過程:
      • 線程遇到同步代碼塊後,線程判斷同步鎖還有沒有
      • 若是同步鎖有:獲取鎖,進入同步中,去執行,執行完畢後,離開同步代碼塊,線程將鎖對象還回去。
      • 在同步中的線程休眠,此時另外一個線程會執行;
      • 遇到同步代碼塊,判斷對象鎖是否還有,若是沒有鎖,該線程不能進入同步代碼塊中執行,被阻擋在同步代碼塊的外面,處於阻塞狀態。
    • 加了同步以後,執行步驟增長:線程首先進同步判斷鎖,獲取鎖,出同步釋放鎖,致使程序運行速度的降低。
    • 沒有鎖的線程,不能進入同步,在同步中的線程,不出同步,不會釋放鎖。

2.2 同步方法(推薦使用)

  • 同步方法:在方法聲明上加上synchronized
public synchronized void method(){
    可能會產生線程安全問題的代碼
}
  • 同步方法中的鎖對象是 this
  • 使用同步方法,對電影院賣票案例中Ticket類進行以下代碼修改:
/*
    採用同步方法的形式解決線程安全問題
    好處:代碼量少,簡潔
    作法:將線程共享數據和同步抽取到方法中
 */
public class Tickets implements Runnable {
    private int num = 100;
    @Override
    public void run() {
        // 死循環,一直處於能夠售票狀態
        while(true) {
            payTicket();
        }
    }
    
    public synchronized void payTicket() {
        if(num>0) { 
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  張票售出");
        }
    }
}
  • 問題:同步方法中有鎖嗎?
    • 有,同步方法中的對象鎖是本類方法的引用
  • 靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){
    // 可能會產生線程安全問題的代碼
}
  • 靜態同步方法中的鎖對象是本類本身:類名.class

1.4 Lock接口

  • 查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可得到的更普遍的鎖定操做。
    • 實現類:ReentrantLock
    • Lock接口中的經常使用方法
      • void lock():得到鎖。
      • void unlock():釋放鎖。
    • Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操做鎖的功能。
  • 咱們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行以下代碼修改:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
    使用JDK1.5+的接口Locl,替換同步代碼塊,實現線程安全
    具體使用:
        Lock接口中的方法:
            lock(); // 獲取鎖
            unlock(); // 釋放鎖
        實現類:ReentrantLock
 */
public class Tickets implements Runnable {
    // 存儲票數
    private static int num = 100;   
    //在類的成員位置,建立Lock接口的實現類對象
    private Lock lock = new ReentrantLock();    
    @Override
    public void run() {
        // 死循環,一直處於能夠售票狀態
        while(true) {
            // 調用Lock接口中的方法,獲取鎖
            lock.lock();
            try {
                if(num>0) {                 
                    Thread.sleep(200);
                    System.out.println(Thread.currentThread().getName()+" 第  "+ num-- + "  張票售出");
                } 
            }catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 釋放鎖,調用unlock方法
                lock.unlock();              
            }
        }
    }   
}

1.4 死鎖

  • 同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,若是同步中嵌套了其餘的同步。這時容易引起一種現象:
    • 程序出現無限等待,這種現象咱們稱爲死鎖。這種狀況能避免就避免掉。
  • 死鎖程序:
    • 前提:必須是多線程
    • 出現同步嵌套
    • 線程進入同步,獲取鎖,不出去同步,不會釋放鎖
  • 鎖的嵌套狀況以下:多線程

    synchronzied(A鎖){
        synchronized(B鎖){
    
        }
    }
    synchronzied(B鎖){
        synchronized(A鎖){
    
        }
    }
  • 注意A鎖和B鎖都是惟一的
  • 兩個線程每一個得到一個鎖,且都須要對方的鎖才能繼續執行,所以都會一直除以阻塞狀態,沒法恢復,出現死鎖。ide

  • 咱們進行下死鎖狀況的代碼演示:測試

    // 定義鎖對象類
    /*
        不容許任何類建立該對象
        只能經過類名調用靜態成員調用,不容許new
        保證了鎖的惟一性
    */
    public class LockA {
        private LockA() {}
        public final static LockA locka = new LockA();
    }
    
    public class LockB {    
        private LockB() {}
        public final static LockB lockb = new LockB();
    }
    
    // 線程任務類
    public class DeadLock implements Runnable{
        private int i = 0;
        @Override
        public void run() {
            while(true) {
                if(i%2==0) {
                    // 先進入A同步,再進入B同步
                    synchronized (LockA.locka) {
                        System.out.println(i+" --> if...locka");
                        synchronized (LockB.lockb) {
                            System.out.println(i+" --> if...lockb");
                        }
                    }
                }else {
                    // 先進入B同步,再進入B同步
                    synchronized (LockB.lockb) {
                        System.out.println(i+" --> else...lockb");
                        synchronized (LockA.locka) {
                            System.out.println(i+" --> else...locka");
                        }
                    }
                }
                i++;
            }
        }
    }
    
    // 測試類
    public class DeadLockDemo {
        public static void main(String[] args) {
            DeadLock deadLock = new DeadLock();
            new Thread(deadLock).start();
            new Thread(deadLock).start();
        }
    }
    
    // 運行結果:
    0 --> if...locka
    0 --> if...lockb
    1 --> else...lockb
    1 --> if...locka

1.5 等待喚醒機制

  • 在開始講解等待喚醒機制以前,有必要搞清一個概念—— 線程之間的通訊。
  • 線程之間的通訊:多個線程在處理同一個資源,可是處理的動做(線程的任務)卻不相同。經過必定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
  • 等待喚醒機制所涉及到的方法:
    • wait() :等待,無限等待。將正在執行的線程釋放其執行資格 和 執行權,並存儲到線程池中。
    • notify() :喚醒。喚醒線程池中被wait()的線程,一次喚醒一個,並且是任意的。
    • notifyAll() :喚醒所有:能夠將線程池中的全部wait()線程都喚醒。
  • 所謂喚醒:就是讓線程池中的線程具有執行資格。
    • 必須注意的是:這些方法都是在同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才能夠明確出這些方法操做的究竟是哪一個鎖上的線程。
  • 仔細查看Java API以後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,爲何這些操做線程的方法定義在Object類中?
    • 由於這些方法在使用時,必需要標明所屬的鎖,而鎖又能夠是任意對象。能被任意對象調用的方法必定定義在Object類中。
  • 線程通信案例:輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:
    1. 當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。若是發現有數據,就wait();
    2. 當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,而後,叫醒input來輸入數據。
  • 下面代碼,模擬等待喚醒機制的實現:
    • Resource.javathis

      /*
          定義資源類,有2個成員變量:
              name,sex
          同時有兩個線程,對資源中的變量操做
          1個對name,sex賦值
          1個對name,sex作變量的輸出打印
       */
      public class Resource {
          public String name;
          public String sex;
      }
    • Input.java線程

      /*
          輸入線程:
              對資源對象Resource中的成員變量賦值
          要求:
              一次賦值:張三,男
              另外一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  if(i%2==0) {
                      r.name = "張三";
                      r.sex = "男";                
                  }else {
                      r.name = "lisi";
                      r.sex = "nv";
                  }
                  i++;
              }
          }
      }
    • Output.java設計

      /*
          輸出線程:對資源對象Resource中的成員變量輸出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  System.out.println("姓名:"+r.name + ", 性別:"+r.sex);
              }
          }
      
      }
    • ThreadDemo.javacode

      /*
          開啓輸入線程和輸出線程,實現賦值和打印
       */
      public class ThreadDemo {
          public static void main(String[] args) {
              Resource r = new Resource();  //共享數據
              Input in = new Input(r);
              Output out = new Output(r);
              new Thread(in).start();
              new Thread(out).start();
          }
      }
  • 此時會出現問題:打印出的結果並非想要的結果

    姓名:lisi, 性別:nv
    姓名:張三, 性別:nv
    姓名:lisi, 性別:男
    姓名:lisi, 性別:nv
    姓名:lisi, 性別:nv
    姓名:張三, 性別:男
  • 分析緣由,兩個線程沒有實現同步。
  • 實現同步的方法:給線程加同步鎖。
    • 注意:給輸入和輸出加的同步鎖應爲同一個對象鎖,而輸入和輸出線程是兩個不一樣的線程,所以不能使用this做爲對象鎖,這裏使用他們公用的資源類Resource對象。
  • 代碼修改以下:
    • Input.java修改

      /*
          輸入線程:
              對資源對象Resource中的成員變量賦值
          要求:
              一次賦值:張三,男
              另外一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {
                      if(i%2==0) {
                          r.name = "張三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                  }
                  i++;
              }
          }
      }
    • Input.java修改

      /*
          輸入線程:
              對資源對象Resource中的成員變量賦值
          要求:
              一次賦值:張三,男
              另外一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {
                      if(i%2==0) {
                          r.name = "張三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                  }
                  i++;
              }
          }
      }
    • Output.java修改:

      /*
          輸出線程:對資源對象Resource中的成員變量輸出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  synchronized (r) {
                      System.out.println("姓名:"+r.name + ", 性別:"+r.sex);               
                  }
              }
          }
      }
  • 此時還有問題:輸出沒有交替進行

    姓名:張三, 性別:男
    姓名:張三, 性別:男
    姓名:張三, 性別:男
    姓名:lisi, 性別:nv
    姓名:lisi, 性別:nv
    姓名:lisi, 性別:nv
    姓名:lisi, 性別:nv
  • 分析緣由:
    • 輸入:輸入完成之後,必須等待,等待輸出打印結束後,才能進行下一次賦值。
    • 輸出:輸出完變量值後,必須等待,等待輸入的從新賦值後,才能進行下一次打印。
  • 解決方法:
    • 輸入:賦值後,執行方法wait(),永遠等待,
    • 輸出:變量打印輸出,在輸出等待以前,喚醒輸入的nitify(),本身再wait等待。
    • 輸入:被喚醒後,從新對變量賦值,而後喚醒輸出的線程notify,本身再wait()等待。
    • 如何判斷輸入輸出結束:設置一個標記flag,以標記爲準;
      • flag = false; 說明賦值完成
      • flag = true; 獲取值完成
    • 輸入操做:
      • 須要不須要賦值,看標記
      • 若是標記爲true,等待
      • 若是標記爲false,不須要等待,賦值
      • 賦值後,將標記改成true
    • 輸出操做:
      • 須要不須要獲取,看標記
      • 如過標記爲false,等待
      • 若是標記爲true,打印
      • 打印後,將標記改成false
  • 代碼修改以下:
    • Resource.java修改

      /*
          定義資源類,有2個成員變量:
              name,sex
          同時有兩個線程,對資源中的變量操做
          1個對name,sex賦值
          1個對name,sex作變量的輸出打印
       */
      public class Resource {
          public String name;
          public String sex;
          public boolean flag = false;
      }
    • Input.java修改

      /*
          輸入線程:
              對資源對象Resource中的成員變量賦值
          要求:
              一次賦值:張三,男
              另外一次:李四,女
      
       */
      public class Input implements Runnable {    
          private Resource r;
          public Input(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              int i = 0;
              while(true) {
                  synchronized (r) {              
                      if(r.flag) { // 標記是true,等待
                          try {
                              r.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      if(i%2==0) {
                          r.name = "張三";
                          r.sex = "男";                
                      }else {
                          r.name = "lisi";
                          r.sex = "nv";
                      }
                      // 標記改成true,將對方線程喚醒
                      r.flag = true;
                      r.notify();
                  }
                  i++;
              }
          }
      }
    • Output.java修改

      /*
          輸出線程:對資源對象Resource中的成員變量輸出值
       */
      public class Output implements Runnable {
          private Resource r;
          public Output(Resource r) {
              this.r = r;
          }
          @Override
          public void run() {
              while(true) {
                  synchronized (r) {              
                      if(!r.flag) {  // 判斷標記,false,等待
                          try {
                              r.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      System.out.println("姓名:"+r.name + ", 性別:"+r.sex);
                      r.flag = false;
                      r.notify();
                  }
              }
          }
      }
  • 注意:
    • 等待和喚醒必須是由同一個對象調用,這裏用Resource的對象

2 總結

同步鎖

  • 多個線程想保證線程安全,必需要使用同一個鎖對象
    • 同步代碼塊

      synchronized (鎖對象){
          可能產生線程安全問題的代碼
      }
  • 同步代碼塊的鎖對象能夠是任意的對象
    • 同步方法

      public synchronized void method()
          可能產生線程安全問題的代碼
      }
      // 同步方法中的鎖對象是 this
    • 靜態同步方法

      public synchronized void method()
          可能產生線程安全問題的代碼
      }
      // 靜態同步方法中的鎖對象是 類名.class

多線程有幾種實現方案,分別是哪幾種?

  • 繼承Thread類
  • 實現Runnable接口
  • 經過線程池,實現Callable接口

同步有幾種方式,分別是什麼?

  • 同步代碼塊
  • 同步方法
  • 靜態同步方法

啓動一個線程是run()仍是start()?它們的區別?

  • 啓動一個線程是start()
  • 區別:
    • start: 啓動線程,並調用線程中的run()方法
    • run : 執行該線程對象要執行的任務

sleep()和wait()方法的區別

  • sleep: 不釋放鎖對象, 釋放CPU使用權;在休眠的時間內,不能喚醒
  • wait(): 釋放鎖對象, 釋放CPU使用權;在等待的時間內,能喚醒

爲何wait(),notify(),notifyAll()等方法都定義在Object類中

  • 鎖對象能夠是任意類型的對象
相關文章
相關標籤/搜索