讀懂Java多線程與并發(fā)-基礎(chǔ)篇

1.幾個(gè)重要概念

同步與異步
同步調(diào)用會(huì)等待方法的返回,異步調(diào)用會(huì)瞬間返回,但是異步調(diào)用瞬間返回并不代表你的任務(wù)就完成了,它會(huì)在后臺(tái)起個(gè)線程繼續(xù)進(jìn)行任務(wù)。
阻塞和非阻塞
阻塞和非阻塞通常形容多線程間的相互影響。比如一個(gè)線程占用了臨界區(qū)資源,那么其它所有需要這個(gè)資源的線程就必須在這個(gè)臨界區(qū)中進(jìn)行等待,等待會(huì)導(dǎo)致線程掛起。這種情況就是阻塞。此時(shí),如果占用資源的線程一直不愿意釋放資源,那么其它所有阻塞在這個(gè)臨界區(qū)上的線程都不能工作。非阻塞允許多個(gè)線程同時(shí)進(jìn)入臨界區(qū)。
并發(fā)和并行
并行則是兩個(gè)任務(wù)同時(shí)進(jìn)行,而并發(fā)呢,則是一會(huì)做一個(gè)任務(wù)一會(huì)又切換到另一個(gè)任務(wù)。所以單個(gè)cpu是不能做到并行的,只能是并發(fā)。
臨界區(qū)
臨界區(qū)用來表示一種公共資源或者說是共享數(shù)據(jù),可以被多個(gè)線程使用,但是每一次,只能有一個(gè)線程使用它,一旦臨界區(qū)資源被占用,其他線程要想使用這個(gè)資源,就必須等待。
死鎖
是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法進(jìn)行下去。比如線程1占了B資源,請(qǐng)求A資源,線程2占了A資源,請(qǐng)求B資源。
活鎖
線程1可以使用資源,但它讓其他線程先使用資源;線程2也可以使用資源,但它也讓其他線程先使用資源,于是兩者一直謙讓,都無法使用資源。

2.使用線程

線程狀態(tài)轉(zhuǎn)換

有三種方法可以創(chuàng)建線程

  • 實(shí)現(xiàn) Runnable 接口;
  • 實(shí)現(xiàn) Callable 接口;
  • 繼承 Thread 類。
public class TestRunnable implements Runnable {
    public void run() {
        
    }
}
public class TestCallable implements Callable<String> {
    public String call() {
        return "HI";
    }
}
public class TestThread extends Thread {
    public void run() {
        
    }
}

實(shí)現(xiàn) Runnable 和 Callable 接口的類只能當(dāng)做一個(gè)可以在線程中運(yùn)行的任務(wù),不是真正意義上的線程,因此最后還需要通過 Thread 來調(diào)用。與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進(jìn)行封裝。

public static void main(String[] args) {
    TestRunnable test1 = new TestRunnable ();
    Thread t1= new Thread(test1);
    t1.start();
    
    TestCallable test2 = new TestCallable();
    FutureTask<String> ft = new FutureTask<>(test2);
    Thread t2= new Thread(ft);
    t2.start();
    System.out.println(ft.get());

    TestThread t3 = new TestThread();
    t3.start();
}

線程中斷
當(dāng)一個(gè)線程正在運(yùn)行時(shí),另一個(gè)線程調(diào)用對(duì)應(yīng)的 Thread 對(duì)象的 interrupt()方法來中斷它,只是在目標(biāo)線程中設(shè)置一個(gè)標(biāo)志,表示它已經(jīng)被中斷,并立即返回。但實(shí)際上線程并沒有被中斷,還會(huì)繼續(xù)往下執(zhí)行。

線程中斷的3個(gè)方法

  • interrupt() 中斷線程
  • isInterrupted() 判斷是否被中斷
  • interrupted() 判斷是否被中斷,并清除當(dāng)前中斷狀態(tài)
public class TestInterrupt  implements Runnable{  
    public void run(){  
        try{  
            System.out.println("in TestInterrupt() - start");  
            Thread.sleep(20000);  
            System.out.println("in TestInterrupt() - woke up");  
        }catch(InterruptedException e){  
            System.out.println("in TestInterrupt() - interrupted");  
        }  
        System.out.println("in TestInterrupt() - leaving");  
    }  

    public static void main(String[] args) {  
        TestInterrupt  test= new TestInterrupt();  
        Thread t = new Thread(test);  
        t.start();  
        //主線程休眠2秒,從而確保剛才啟動(dòng)的線程有機(jī)會(huì)執(zhí)行一段時(shí)間  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("in main() - interrupting other thread");  
        //中斷線程t  
        t.interrupt();  
        System.out.println("in main() - leaving");  
    }  
} 

最后會(huì)輸出 in TestInterrupt () - leaving。

3.線程間協(xié)作

線程等待與喚醒
調(diào)用 wait() 使得線程被掛起,當(dāng)其他線程調(diào)用 notify() 或者 notifyAll() 來喚醒掛起的線程。它們都屬于Object,而不屬于 Thread。

public class TestWait {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }
    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    TestWait test = new TestWait ();
     new Thread(()->test.after()).start();
     new Thread(()->test.before()).start();
}

wait() 和 sleep() 的區(qū)別

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態(tài)方法;
  • wait() 會(huì)釋放鎖,sleep() 不會(huì)。

java.util.concurrent 類庫中提供了 Condition 類,可調(diào)用 await() 方法使線程等待,其它線程調(diào)用 signal() 或 signalAll() 方法喚醒等待的線程。用法跟上面的類似,只是把同步塊synchronized換成了同步鎖ReentrantLock。
兩者的區(qū)別:

  • synchronized 是 JVM 實(shí)現(xiàn)的,而 ReentrantLock 是 JDK 實(shí)現(xiàn)的。
  • synchronized 是非公平的,ReentrantLock 默認(rèn)情況下也是非公平的,但是可以實(shí)現(xiàn)公平鎖。
  • ReentrantLock 等待可中斷,而 synchronized 不行。
  • ReentrantLock 可以同時(shí)綁定多個(gè)條件Condition。
  • ReentrantLock 可重入,一個(gè)線程可以反復(fù)得到相同的一把鎖。
public class TestAwait{

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

join()
在線程中調(diào)用另一個(gè)線程的 join() 方法,會(huì)將當(dāng)前線程掛起,而不是忙等待,直到目標(biāo)線程結(jié)束。

public static class TestJoin {
    int num=0;  
    public void  printNum(){
        for (int i = 0; i < 100; i++){
            num=i;
        }
    }
    
    public static void main(String[] args) {
        TestJoin test = new TestJoin ();
        Thread t=new Thread(()->test.printNum()).start();
                 t.join();
                 System.out.println(test.num);
    }
}

4.內(nèi)存模型

主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量主要是指共享變量。Java 內(nèi)存模型規(guī)定所有的變量都存儲(chǔ)在主內(nèi)存中,而每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值得傳遞均需要通過主內(nèi)存來完成。


Java 內(nèi)存模型中定義了以下 8 種操作來完成主內(nèi)存與工作內(nèi)存之間交互的實(shí)現(xiàn)細(xì)節(jié)。

  • luck(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)示為一條線程獨(dú)占的狀態(tài)。
  • unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  • read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中,以便隨后的 load 動(dòng)作使用。
  • load(載入):作用于工作內(nèi)存的變量,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  • use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值得字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
  • assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
  • store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳遞到主內(nèi)存中,以便隨后的 write 操作使用。
  • write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。

三大特性

  • 原子性:指一個(gè)操作是不可中斷的。即使是在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開始,就不會(huì)被其它線程干擾。
  • 有序性:在并發(fā)時(shí),程序的執(zhí)行可能就會(huì)出現(xiàn)亂序。因?yàn)橹噶钣锌赡鼙恢嘏拧?/li>
  • 內(nèi)存可見性:指當(dāng)一個(gè)線程修改了某一個(gè)共享變量的值,其他線程是否能夠立即知道這個(gè)修改。

5.volatile和ThreadLocal

volatile 修飾的成員變量在每次被線程訪問時(shí),都強(qiáng)迫從共享內(nèi)存中重讀該成員變量的值。而且,當(dāng)成員變量發(fā)生變化時(shí),強(qiáng)迫線程將變化值回寫到共享內(nèi)存。這樣在任何時(shí)刻,兩個(gè)不同的線程總是看到某個(gè)成員變量的同一個(gè)值。
volatile 是一種稍弱的同步機(jī)制,在訪問 volatile 變量時(shí)不會(huì)執(zhí)行加鎖操作,也就不會(huì)執(zhí)行線程阻塞,因此 volatilei 變量是一種比 synchronized 關(guān)鍵字更輕量級(jí)的同步機(jī)制。
volatile 雖然保證了內(nèi)存可見性和有序性(添加內(nèi)存屏障的方式來禁止指令重排),但不能保證原子性,所以是線程不安全的。

ThreadLocal是解決線程安全問題一個(gè)很好的思路,ThreadLocal類中有一個(gè)Map,用于存儲(chǔ)每一個(gè)線程的變量副本,Map中元素的鍵為線程對(duì)象,而值對(duì)應(yīng)線程的變量副本,由于Key值不可重復(fù),每一個(gè)“線程對(duì)象”對(duì)應(yīng)線程的“變量副本”,而到達(dá)了線程安全。

public class TestThreadLocal{

//通過匿名內(nèi)部類覆蓋ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){

      public Integer initialValue(){
            return 0;
    }

};

public int getNextNum(){

    seqNum.set(seqNum.get()+1);

     return seqNum.get();

}

public static void main(String[] args) {
    
    TestThreadLocal test = new TestThreadLocal();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
     new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
  }
}

ThreadLocal和線程同步機(jī)制比較

  • 同步機(jī)制中,通過對(duì)象的鎖機(jī)制保證同一時(shí)間只有一個(gè)線程訪問變量。這時(shí)該變量是多個(gè)線程共享的。
  • ThreadLocal會(huì)為每一個(gè)線程拷貝一份獨(dú)立的變量副本,從而隔離了多個(gè)線程對(duì)數(shù)據(jù)的訪問沖突。
    當(dāng)然ThreadLocal并不能替代同步機(jī)制,兩者面向的問題領(lǐng)域不同。

參考資料
高并發(fā)Java
線程通信

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

相關(guān)閱讀更多精彩內(nèi)容

  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認(rèn)情況下,在創(chuàng)建了線程池之后...
    irckwk1閱讀 873評(píng)論 0 0
  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機(jī)不和包括java...
    kennethan閱讀 1,070評(píng)論 0 2
  • 1. 計(jì)算機(jī)系統(tǒng) 使用高速緩存來作為內(nèi)存與處理器之間的緩沖,將運(yùn)算需要用到的數(shù)據(jù)復(fù)制到緩存中,讓計(jì)算能快速進(jìn)行;當(dāng)...
    AI喬治閱讀 586評(píng)論 0 12
  • 最近在摸索如何edit出realistic風(fēng)格的sims圖片,結(jié)果還是失敗了...不知道是技術(shù)不夠還是沒有用HQ ...
    黑盒君閱讀 299評(píng)論 0 0
  • 色彩搭配原理與技巧一、從頭到腳一般不能超過三種顏色 1、色彩的搭配方法 1、上深下淺:端莊、大方、恬靜、嚴(yán)肅 2、...
    Michelle_zhuge閱讀 595評(píng)論 0 1

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