多線程(二)——多線程同步安全問題

為什么有線程安全問題?

當(dāng)多個線程同時共享,同一個全局變量或靜態(tài)變量,做寫的操作時,可能會發(fā)生數(shù)據(jù)沖突問題,也就是線程安全問題。但是做讀操作是不會發(fā)生數(shù)據(jù)沖突問題。
案例:需求現(xiàn)在有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。

  • 代碼:
class ThreadTrain1 implements Runnable {
  private int count = 100;
  private static Object oj = new Object();

  @Override
  public void run() {
    while (count > 0) {
        try {
            Thread.sleep(50);
        } catch (Exception e) {
            // TODO: handle exception
        }
        sale();
    }
  }

  public void sale() {
    // 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。
    // 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低
    // synchronized (oj) {
        if (count > 0) {
          System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
          count--;
        }
    // }
  }
}

public class ThreadDemo {
  public static void main(String[] args) {
    ThreadTrain1 threadTrain1 = new ThreadTrain1();
    Thread t1 = new Thread(threadTrain1, "①號窗口");
    Thread t2 = new Thread(threadTrain1, "②號窗口");
    t1.start();
    t2.start();
  }
}

結(jié)論發(fā)現(xiàn),多個線程共享同一個全局成員變量時,做寫的操作可能會發(fā)生數(shù)據(jù)沖突問題。

線程安全解決辦法

問:如何解決多線程之間線程安全問題?
答:使用多線程之間同步synchronized或使用鎖(lock)。

問:為什么使用線程同步或使用鎖能解決線程安全問題呢?
答:將可能會發(fā)生數(shù)據(jù)沖突問題(線程不安全問題),只能讓當(dāng)前一個線程進(jìn)行執(zhí)行。代碼執(zhí)行完成后釋放鎖,然后才能讓其他線程進(jìn)行執(zhí)行。這樣的話就可以解決線程不安全問題。

問:什么是多線程之間同步?
答:當(dāng)多個線程共享同一個資源,不會受到其他線程的干擾。

同步代碼塊

  • 什么是同步代碼塊?
    就是將可能會發(fā)生線程安全問題的代碼,給包括起來。
    synchronized(同一個數(shù)據(jù)){
    可能會發(fā)生線程沖突問題
    }
    這就是同步代碼塊
定義:

synchronized(對象)      //這個對象可以為任意對象 
{ 
   需要被同步的代碼 
} 

對象如同鎖,持有鎖的線程可以在同步中執(zhí)行 ,沒持有鎖的線程即使獲取CPU的執(zhí)行權(quán),也進(jìn)不去 。

  • 同步的前提:
    1,必須要有兩個或者兩個以上的線程
    2,必須是多個線程使用同一個鎖
    3,必須保證同步中只能有一個線程在運(yùn)行
    好處:解決了多線程的安全問題
    弊端:多個線程需要判斷鎖,較為消耗資源、搶鎖的資源。

    代碼樣例:
    private static Object oj = new Object();      
    public void sale() {
      // 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。
      // 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低
       synchronized (oj) {
        if (count > 0) {
          System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
          count--;
        }
       }
    }
    

同步函數(shù)

  • 同步函數(shù):在方法上修飾synchronized 稱為同步函數(shù)。
  • 同步函數(shù)用的是什么鎖?為什么?
    答:同步函數(shù)使用this鎖。
    證明方式: 一個線程使用同步代碼塊(this明鎖),另一個線程使用同步函數(shù)。如果兩個線程搶票不能實(shí)現(xiàn)同步,那么會出現(xiàn)數(shù)據(jù)錯誤。
  • 代碼:
class ThreadTrain2 implements Runnable {
private int count = 100;
public boolean flag = true;
private static Object oj = new Object();

@Override
public void run() {
    if (flag) {

        while (count > 0) {

            synchronized (this) {
                if (count > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                    count--;
                }
            }

        }

    } else {
        while (count > 0) {
            sale();
        }
    }

}

public synchronized void sale() {
    // 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。
    // 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低
    // synchronized (oj) {
    if (count > 0) {
        try {
            Thread.sleep(50);
        } catch (Exception e) {
            // TODO: handle exception
        }
        System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
        count--;
    }
    // }
  }
}

public class ThreadDemo2 {
  public static void main(String[] args) throws InterruptedException {
    ThreadTrain2 threadTrain1 = new ThreadTrain2();
    Thread t1 = new Thread(threadTrain1, "①號窗口");
    Thread t2 = new Thread(threadTrain1, "②號窗口");
    t1.start();
    Thread.sleep(40);
    threadTrain1.flag = false;
    t2.start();
  }
 }

靜態(tài)同步函數(shù)

靜態(tài)同步函數(shù):方法上加上static關(guān)鍵字,使用synchronized 關(guān)鍵字修飾,使用類.class文件作為鎖對象。
靜態(tài)的同步函數(shù)使用的鎖為該函數(shù)所屬字節(jié)碼文件對象,可以用 getClass方法獲取,也可以用當(dāng)前 類名.class 表示。

代碼樣例:

synchronized (ThreadTrain.class) {
        System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
        trainCount--;
        try {
            Thread.sleep(100);
        } catch (Exception e) {
        }
}

面試常問:
一個線程使用同步函數(shù),另一個線程使用同步代碼塊(this),能夠?qū)崿F(xiàn)同步。
一個線程使用同步函數(shù),另一個線程使用同步代碼塊(非this),不能實(shí)現(xiàn)同步。
一個線程使用同步函數(shù),另一個線程使用靜態(tài)同步函數(shù),不能實(shí)現(xiàn)同步。
總結(jié):
同步函數(shù)使用this鎖;
同步代碼塊可使用任意對象鎖或者this鎖;
靜態(tài)同步函數(shù)使用類的字節(jié)碼.class文件鎖。

多線程死鎖

  • 多線程死鎖:同步中嵌套同步,導(dǎo)致鎖無法釋放

  • 代碼:

    class ThreadTrain6 implements Runnable {
    // 這是貨票總票數(shù),多個線程會同時共享資源
    private int trainCount = 100;
    public boolean flag = true;
    private Object obj= new Object();
    
    @Override
    public void run() {
      if (flag) {
          while (true) {
              synchronized (obj) {
                  // 鎖(同步代碼塊)在什么時候釋放? 代碼執(zhí)行完, 自動釋放鎖.
                  // 如果flag為true 先拿到 obj鎖,在拿到this 鎖、 才能執(zhí)行。
                  // 如果flag為false先拿到this,在拿到obj鎖,才能執(zhí)行。
                  // 死鎖解決辦法:不要在同步中嵌套同步。
                  sale();
              }
          }
      } else {
          while (true) {
              sale();
          }
      }
    }
    
    public synchronized void sale() {
      synchronized (obj) {
          if (trainCount > 0) {
              try {
                  Thread.sleep(40);
              } catch (Exception e) {
    
              }
              System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
              trainCount--;
          }
        }
      }
    }
    
    public class DeadlockThread {
      public static void main(String[] args) throws InterruptedException {
        ThreadTrain6 threadTrain = new ThreadTrain6(); // 定義 一個實(shí)例
        Thread thread1 = new Thread(threadTrain, "一號窗口");
        Thread thread2 = new Thread(threadTrain, "二號窗口");
        thread1.start();
        Thread.sleep(40);
        threadTrain.flag = false;
        thread2.start();
      }
    }
    

原因分析:
線程一先拿到同步代碼塊的obj鎖,再拿到同步函數(shù)的this鎖;
線程一先拿到同步函數(shù)的this鎖,再拿到同步代碼塊的obj鎖,
每個線程都需要對方的鎖,但又互不讓鎖,就會導(dǎo)致死鎖。

多線程有三大特性

原子性、可見性、有序性

什么是原子性

即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
一個很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題: 比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
我們操作數(shù)據(jù)也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運(yùn)行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實(shí)就是保證數(shù)據(jù)一致、線程安全一部分,

什么是可見性

當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
若兩個線程在不同的cpu,那么線程1改變了i的值還沒刷新到主存,線程2又使用了i,那么這個i值肯定還是之前的,線程1對變量的修改線程沒看到這就是可見性問題。

什么是有序性

程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
一般來說處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。如下:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
則因?yàn)橹嘏判?,他還可能執(zhí)行順序?yàn)?2-1-3-4,1-3-2-4 但絕不可能 2-1-4-3,因?yàn)檫@打破了依賴關(guān)系。 顯然重排序?qū)尉€程運(yùn)行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。

Java內(nèi)存模型

共享內(nèi)存模型指的就是Java內(nèi)存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:
1. 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
2. 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。

下面通過示意圖來說明這兩個步驟:


如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時,這三個內(nèi)存中的x值都為0。線程A在執(zhí)行時,把更新后的x值(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來看,這兩個步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為java程序員提供內(nèi)存可見性保證。

總結(jié)
Java內(nèi)存模型:java內(nèi)存模型簡稱jmm,定義了一個線程對另一個線程可見。共享變量存放在主內(nèi)存中,每個線程都有自己的本地內(nèi)存,當(dāng)多個線程同時訪問一個數(shù)據(jù)的時候,可能本地內(nèi)存沒有及時刷新到主內(nèi)存,所以就會發(fā)生線程安全問題。
Java內(nèi)存結(jié)構(gòu):是屬于jvm內(nèi)存分配,不要和Java內(nèi)存模型搞混。

volatile

volatile 關(guān)鍵字的作用是變量在多個線程之間可見。

  • 代碼:
classThreadVolatileDemo extends Thread {

  public boolean flag= true;

  @Override

  public void run() {

    System.out.println("開始執(zhí)行子線程....");

    while (flag) {

    }

    System.out.println("線程停止");

}

  public void setRuning(boolean flag) {

    this.flag= flag;

  }

}

public class ThreadVolatile {

  public static voidmain(String[] args) throws InterruptedException {

    ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();

    threadVolatileDemo.start();

    Thread.sleep(3000);

    threadVolatileDemo.setRuning(false);

    System.out.println("flag 已經(jīng)設(shè)置成false");

    Thread.sleep(1000);

    System.out.println(threadVolatileDemo.flag);

  }

}

已經(jīng)將結(jié)果設(shè)置為fasle為什么?還一直在運(yùn)行呢。
原因:線程之間是不可見的,讀取的是副本,沒有及時讀取到主內(nèi)存結(jié)果。
解決辦法:使用volatile關(guān)鍵字將解決線程之間可見性, 強(qiáng)制線程每次讀取該值的時候都去“主內(nèi)存”中取值。

Volatile非原子性

注意: Volatile非原子性

public class VolatileNoAtomic extends Thread {

  private static volatile int count;

  // private static AtomicInteger count = new AtomicInteger(0);

  private static void addCount() {

    for (int i = 0;i< 1000;i++) {
      count++;
      // count.incrementAndGet();
    }
    System.out.println(count);
  }
  public void run() {
    addCount();
  }
  public static void main(String[] args) {
    VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
    for (int i = 0; i < 10; i++) {
        arr[i] = new VolatileNoAtomic();
    }
    for (int i = 0; i < 10; i++) {
        arr[i].start();
    }
  }
}

運(yùn)行結(jié)果:



結(jié)果發(fā)現(xiàn) 數(shù)據(jù)不同步,因?yàn)閂olatile不用具備原子性。

使用AtomicInteger原子類
AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減。(JDK1.5并發(fā)包中的類)

public class VolatileNoAtomic extends Thread {
  static int count = 0;
  private static AtomicInteger atomicInteger = new AtomicInteger(0);

  @Override
  public void run() {
    for (int i = 0; i < 1000; i++) {
        //等同于i++
        atomicInteger.incrementAndGet();
    }
    System.out.println(count);
  }

  public static void main(String[] args) {
    // 初始化10個線程
    VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
    for (int i = 0; i < 10; i++) {
        // 創(chuàng)建
        volatileNoAtomic[i] = new VolatileNoAtomic();
    }
    for (int i = 0; i < volatileNoAtomic.length; i++) {
        volatileNoAtomic[i].start();
    }
  }
}

volatile與synchronized區(qū)別

僅靠volatile不能保證線程的安全性。(原子性)
①volatile輕量級,只能修飾變量。
synchronized重量級,還可修飾方法
②volatile只能保證數(shù)據(jù)的可見性,不能用來同步,因?yàn)槎鄠€線程并發(fā)訪問volatile修飾的變量不會阻塞。
synchronized不僅保證可見性,而且還保證原子性(數(shù)據(jù)一致),因?yàn)?,只有獲得了鎖的線程才能進(jìn)入臨界區(qū),從而保證臨界區(qū)中的所有語句都全部執(zhí)行。多個線程爭搶synchronized鎖對象時,會出現(xiàn)阻塞。
③線程安全性
線程安全性包括兩個方面:1)可見性,2)原子性。
從上面自增的例子中可以看出:僅僅使用volatile并不能保證線程安全性。而synchronized則可實(shí)現(xiàn)線程的安全性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容