java多線程相關問題彙總

1、線程和進程的關係

根本區別:進程是操作系統資源分配的基本單位,而線程是處理器任務調度和執行的基本單位。

資源開銷:每個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程可以看做輕量級的進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。

包含關係:如果一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,所以線程也被稱爲輕權進程或者輕量級進程。

內存分配:同一進程的線程共享本進程的地址空間和資源,而進程之間的地址空間和資源是相互獨立的。

影響關係:一個進程崩潰後,在保護模式下不會對其他進程產生影響,但是一個線程崩潰整個進程都死掉。所以多進程要比多線程健壯。

2、線程有哪些狀態

a. 新建(new):新創建了一個線程對象。

b. 可運行(runnable):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。

c. 運行(running):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。

d. 阻塞(block):阻塞狀態是指線程因爲某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,纔有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:

(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。

(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。

(三). 其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。

e. 死亡(dead):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。
在這裏插入圖片描述
鎖池和等待池

在Java中,每個對象都有兩個池,鎖(monitor)池和等待池

鎖池: 假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。

等待池: 假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因爲wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。如果另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會全部進入該對象的鎖池中,準備爭奪鎖的擁有權。如果另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池。
在這裏插入圖片描述

3、synchronized 底層實現原理

synchronized的三種應用方式

修飾實例方法:作用於當前對象加鎖,進入同步代碼前要獲得當前對象的鎖。

修飾靜態方法:作用於當前類加鎖,進入同步代碼前要獲得當前類的鎖。

當synchronized作用於靜態方法時,其鎖就是當前類的class對象鎖。由於靜態成員不專屬於任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態 成員的併發操作。需要注意的是如果一個線程A調用一個實例對象的非static synchronized方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。

修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

synchronized底層原理

Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現, 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充
在這裏插入圖片描述
實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。

填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解即可。

而對於頂部,則是Java頭對象,它實現synchronized的鎖對象的基礎,這點我們重點分析它,一般而言,synchronized使用的鎖對象是存儲在Java對象頭裏的,jvm中採用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成,其結構說明如下表:
在這裏插入圖片描述
其中Mark Word在默認情況下存儲着對象的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word默認存儲結構
在這裏插入圖片描述
由於對象頭的信息是與對象自身定義的數據沒有關係的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設計成爲一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象本身的狀態複用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結構外,還有如下可能變化的結構:
在這裏插入圖片描述

指針指向的是monitor對象(也稱爲管程或監視器鎖)的起始地址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)

3: monitorenter  //進入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程可以成功取得 monitor,並將計數器值設置爲 1,取鎖成功。如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。
倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。

爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

4、synchronized 和 Lock區別

a、Synchronized是關鍵字,內置語言實現,Lock是接口。

b、Synchronized在線程發生異常時會自動釋放鎖,因此不會發生異常死鎖。Lock異常時不會自動釋放鎖,所以需要在finally中實現釋放鎖。

c、Lock是可以中斷鎖,Synchronized是非中斷鎖,必須等待線程執行完成釋放鎖。

d、Lock可以使用讀鎖提高多線程讀效率。
在這裏插入圖片描述

一、synchronized和lock的用法區別

synchronized:在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號中表示需要鎖的對象。

lock:需要顯示指定起始位置和終止位置。
一般使用ReentrantLock類做爲鎖,多個線程中必須要使用一個ReentrantLock類做爲對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。

用法區別比較簡單,這裏不贅述了,如果不懂的可以看看Java基本語法。

二、synchronized和lock性能區別

synchronized是託管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因爲這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化餘地。

說到這裏,還是想提一下這2中機制的具體區別。據我所知,synchronized原始採用的是CPU悲觀鎖機制,即線程獲得的是獨佔鎖。獨佔鎖意味着其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程上下文切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。

而Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。我們可以進一步研究ReentrantLock的源代碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這裏其實就是調用的CPU提供的特殊指令。

現代的CPU提供了指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。這個算法稱作非阻塞算法,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。

5、怎麼防止死鎖?

雖然進程在運行過程中,可能發生死鎖,但死鎖的發生也必須具備一定的條件,死鎖的發生必須具備以下四個必要條件。

1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。

2)請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。

3)不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。

4)環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

在系統中已經出現死鎖後,應該及時檢測到死鎖的發生,並採取適當的措施來解除死鎖。目前處理死鎖的方法可歸結爲以下四種:

  1. 預防死鎖。

這是一種較簡單和直觀的事先預防的方法。方法是通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或者幾個,來預防發生死鎖。預防死鎖是一種較易實現的方法,已被廣泛使用。但是由於所施加的限制條件往往太嚴格,可能會導致系統資源利用率和系統吞吐量降低。

  1. 避免死鎖。

該方法同樣是屬於事先預防的策略,但它並不須事先採取各種限制措施去破壞產生死鎖的的四個必要條件,而是在資源的動態分配過程中,用某種方法去防止系統進入不安全狀態,從而避免發生死鎖。

3)檢測死鎖。

這種方法並不須事先採取任何限制性措施,也不必檢查系統是否已經進入不安全區,此方法允許系統在運行過程中發生死鎖。但可通過系統所設置的檢測機構,及時地檢測出死鎖的發生,並精確地確定與死鎖有關的進程和資源,然後採取適當措施,從系統中將已發生的死鎖清除掉。

4)解除死鎖。

這是與檢測死鎖相配套的一種措施。當檢測到系統中已發生死鎖時,須將進程從死鎖狀態中解脫出來。常用的實施方法是撤銷或掛起一些進程,以便回收一些資源,再將這些資源分配給已處於阻塞狀態的進程,使之轉爲就緒狀態,以繼續運行。死鎖的檢測和解除措施,有可能使系統獲得較好的資源利用率和吞吐量,但在實現上難度也最大。

6、線程池都有哪些狀態?

在這裏插入圖片描述

1.RUNNING:這是最正常的狀態,接受新的任務,處理等待隊列中的任務。線程池的初始化狀態是RUNNING。線程池被一旦被創建,就處於RUNNING狀態,並且線程池中的任務數爲0。

2.SHUTDOWN:不接受新的任務提交,但是會繼續處理等待隊列中的任務。調用線程池的shutdown()方法時,線程池由RUNNING -> SHUTDOWN。

3.STOP:不接受新的任務提交,不再處理等待隊列中的任務,中斷正在執行任務的線程。調用線程池的shutdownNow()方法時,線程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING:所有的任務都銷燬了,workCount 爲 0,線程池的狀態在轉換爲 TIDYING 狀態時,會執行鉤子方法 terminated()。因爲terminated()在ThreadPoolExecutor類中是空的,所以用戶想在線程池變爲TIDYING時進行相應的處理;可以通過重載terminated()函數來實現。

當線程池在SHUTDOWN狀態下,阻塞隊列爲空並且線程池中執行的任務也爲空時,就會由 SHUTDOWN -> TIDYING。

當線程池在STOP狀態下,線程池中執行的任務爲空時,就會由STOP -> TIDYING。

5.TERMINATED:線程池處在TIDYING狀態時,執行完terminated()之後,就會由 TIDYING -> TERMINATED。

在這裏插入圖片描述
1.引言 合理利用線程池能夠帶來三個好處。第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。第二:提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。但是要做到合理的利用線程池,必須對其原理了如指掌。

2.線程池使用 Executors提供的四種線程 1.newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。 2.newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。 3.newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。 4.newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

1.newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。示例如下

ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +  "," +index);
        }
    });
}

//控制檯信息
pool-1-thread-1,0
pool-1-thread-1,1
pool-1-thread-1,2
pool-1-thread-1,3
pool-1-thread-1,4
2.newFixedThreadPool創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。示例如下

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
for(int i=0;i<5;i++) {
    final int index = i;
       fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + ", " + index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

//控制檯信息
pool-1-thread-1,0
pool-1-thread-2,1
pool-1-thread-3,2
pool-1-thread-4,3
pool-1-thread-1,4
3.newScheduledThreadPool 創建一個定長線程池,支持週期和定時任務示例如下

ScheduledExecutorService scheduledThreadPool =  Executors.newScheduledThreadPool(5);
System.out.println("before:" + System.currentTimeMillis()/1000);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("延遲3秒執行的哦 :" + System.currentTimeMillis()/1000);
    }
}, 3, TimeUnit.SECONDS);
System.out.println("after :" +System.currentTimeMillis()/1000);

//控制檯信息
before:1518012703
after :1518012703
延遲3秒執行的哦 :1518012706

System.out.println("before:" + System.currentTimeMillis()/1000);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("延遲1秒之後,3秒執行一次:" +System.currentTimeMillis()/1000);
    }
}, 1, 3, TimeUnit.SECONDS);
System.out.println("after :" +System.currentTimeMillis()/1000);

控制檯消息
before:1518013024
after :1518013024
延遲1秒之後,3秒執行一次:1518013025
延遲1秒之後,3秒執行一次:1518013028
延遲1秒之後,3秒執行一次:1518013031
4.newSingleThreadExecutor創建一個單線程化的線程池,只會用工作線程來執行任務,保證順序,示例如下

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i=0;i<10;i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            try {
                  System.out.println(Thread.currentThread().getName() + "," + index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

//控制檯信息
pool-1-thread-1,0
pool-1-thread-1,1
pool-1-thread-1,2
pool-1-thread-1,3
pool-1-thread-1,4
向線程池提交任務 ThreadPoolExecutor類中execute()和submit()區別 execute()方法實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執行。

submit()方法是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並沒有對其進行重寫,這個方法也是用來向線程池提交任務的,但是它和execute()方法不同,它能夠返回任務執行的結果,通過源碼查看submit()方法的實現,會發現它實際上還是調用的execute()方法,只不過它利用了Future來獲取任務執行結果。

/**
 * @throws RejectedExecutionException {@inheritDoc}
 * @throws NullPointerException       {@inheritDoc}
 */
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

線程池的關閉 我們可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池,但是它們的實現原理不同,shutdown的原理是隻是將線程池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。shutdownNow的原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。shutdownNow會首先將線程池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表。

只要調用了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於我們應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown來關閉線程池,如果任務不一定要執行完,則可以調用shutdownNow。

  1. 線程池的分析 流程分析:線程池的主要工作流程如下圖: Java線程池主要工作流程

從上圖我們可以看出,當提交一個新任務到線程池時,線程池的處理流程如下:

首先線程池判斷基本線程池是否已滿?沒滿,創建一個工作線程來執行任務。滿了,則進入下個流程。
其次線程池判斷工作隊列是否已滿?沒滿,則將新提交的任務存儲在工作隊列裏。滿了,則進入下個流程。
最後線程池判斷整個線程池是否已滿?沒滿,則創建一個新的工作線程來執行任務,滿了,則交給飽和策略來處理這個任務。
**源碼分析。**上面的流程分析讓我們很直觀的瞭解的線程池的工作原理,讓我們再通過源代碼來看看是如何實現的。線程池執行任務的方法如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

在這裏插入圖片描述

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