Android复习系列②之《Java进阶》
1 java多線程(非常重要)
1.1. 線程
線程和進(jìn)程的區(qū)別?
線程是CPU調(diào)度的最小單位,一個(gè)進(jìn)程中可以包含多個(gè)線程,在Android中,一個(gè)進(jìn)程通常是一個(gè)App,App中會(huì)有一個(gè)主線程,主線程可以用來操作界面元素,如果有耗時(shí)的操作,必須開啟子線程執(zhí)行,不然會(huì)出現(xiàn)ANR,除此以外,進(jìn)程間的數(shù)據(jù)是獨(dú)立的,線程間的數(shù)據(jù)可以共享。
java多線程實(shí)現(xiàn)方式主要有:
繼承Thread
優(yōu)點(diǎn) : 方便傳參,可以在子類添加成員變量,通過方法設(shè)置參數(shù)或構(gòu)造函數(shù)傳參。
缺點(diǎn):
1.因?yàn)镴ava不支持多繼承,所以繼承了Thread類以后,就無法繼承其他類。
2.每次都要新建一個(gè)類,不支持通過線程池操作,創(chuàng)建和銷毀線程對(duì)資源的開銷比較大。
3.從代碼結(jié)構(gòu)上講,為了啟動(dòng)一個(gè)線程任務(wù),都要?jiǎng)?chuàng)建一個(gè)類,耦合性太高。
4.無法獲取線程任務(wù)的返回結(jié)果。
實(shí)現(xiàn)Runnable
優(yōu)點(diǎn) : 此方式可以繼承其他類。也可以使用線程池管理,節(jié)約資源。創(chuàng)建線程代碼的耦合性較低。推薦使用此種方式創(chuàng)建線程。
缺點(diǎn): 不方便傳參,只能使用主線程中用final修飾的變量。其次是無法獲取線程任務(wù)的返回結(jié)果。
實(shí)現(xiàn)Callable
此種方式創(chuàng)建線程底層源碼也是使用實(shí)現(xiàn)Runnable接口的方式實(shí)現(xiàn)的,所以不是一種新的創(chuàng)建線程的方式,只是在實(shí)現(xiàn)Runnable接口方式創(chuàng)建線程的基礎(chǔ)上,同時(shí)實(shí)現(xiàn)了Future接口,實(shí)現(xiàn)有返回值的創(chuàng)建線程。
Runnable 與 Callable的區(qū)別:
1. Runnable是在JDK1.0的時(shí)候提出的多線程的實(shí)現(xiàn)接口,而Callable是在JDK1.5之后提出的; 2. Runnable 接口之中只提供了一個(gè)run()方法,并且沒有返回值; 3. Callable接口提供有call(),可以有返回值;擴(kuò)展:
Callable接口支持返回執(zhí)行結(jié)果,此時(shí)需要調(diào)用FutureTask.get()方法實(shí)現(xiàn),此方法會(huì)阻塞主線程直到獲取‘將來’結(jié)果; 當(dāng)不調(diào)用此方法時(shí),主線程不會(huì)阻塞public class CallableImpl implements Callable<String> {public CallableImpl(String acceptStr) {this.acceptStr = acceptStr;}private String acceptStr;@Overridepublic String call() throws Exception {// 任務(wù)阻塞 1 秒Thread.sleep(1000);return this.acceptStr + " append some chars and return it!";}public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<String> callable = new CallableImpl("my callable test!");FutureTask<String> task = new FutureTask<>(callable);long beginTime = System.currentTimeMillis();// 創(chuàng)建線程new Thread(task).start();// 調(diào)用get()阻塞主線程,反之,線程不會(huì)阻塞String result = task.get();long endTime = System.currentTimeMillis();System.out.println("hello : " + result);System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");} }//執(zhí)行結(jié)果hello : my callable test! append some chars and return it! cast : 1 second!總結(jié):
根據(jù)Oracle提供的JAVA官方文檔的說明,Java創(chuàng)建線程的方法只有兩種方式,即繼承Thread類和實(shí)現(xiàn)Runnable接口。其他所有創(chuàng)建線程的方式,底層都是使用這兩種方式中的一種實(shí)現(xiàn)的,比如通過線程池、通過匿名類、通過lambda表達(dá)式、通過Callable接口等等,全是通過這兩種方式中的一種實(shí)現(xiàn)的。所以我們?cè)谡莆站€程創(chuàng)建的時(shí)候,必須要掌握的只有這兩種,通過文章中優(yōu)缺點(diǎn)的分析,這兩種方法中,最為推薦的就是實(shí)現(xiàn)Runnable接口的方式去創(chuàng)建線程。
1.2. 線程的狀態(tài)有哪些?
Java中定義線程的狀態(tài)有6種,可以查看Thread類的State枚舉:
public static enum State{NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;private State() {}}- 初始(NEW):新創(chuàng)建了一個(gè)線程對(duì)象,還沒調(diào)用start方法;
- 運(yùn)行(RUNNABLE):java線程中將就緒(ready)和運(yùn)行中(running)統(tǒng)稱為運(yùn)行(RUNNABLE)。線程創(chuàng)建后調(diào)用了該對(duì)象的start方法,此時(shí)處于就緒狀態(tài),當(dāng)獲得CPU時(shí)間片后變?yōu)檫\(yùn)行中狀態(tài);
- 阻塞(BLOCKED):表現(xiàn)線程阻塞于鎖;
- 等待(WAITING):進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷);
- 超時(shí)等待(TIMED_WAITING):該狀態(tài)不同于WAITING,它可以在指定時(shí)間后自行返回;
- 終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完畢。
狀態(tài)詳細(xì)說明:
初始狀態(tài)(NEW)
實(shí)現(xiàn)Runnable接口和繼承Thread可以得到一個(gè)線程類,new一個(gè)實(shí)例出來,線程就進(jìn)入了初始狀態(tài)。
就緒狀態(tài)(RUNNABLE之READY)
就緒狀態(tài)只是說你資格運(yùn)行,調(diào)度程序沒有挑選到你,你就永遠(yuǎn)是就緒狀態(tài)。
調(diào)用線程的start()方法,此線程進(jìn)入就緒狀態(tài)。
當(dāng)前線程sleep()方法結(jié)束,其他線程join()結(jié)束,等待用戶輸入完畢,某個(gè)線程拿到對(duì)象鎖,這些線程也將進(jìn)入就緒狀態(tài)。
當(dāng)前線程時(shí)間片用完了,調(diào)用當(dāng)前線程的yield()方法,當(dāng)前線程進(jìn)入就緒狀態(tài)。
鎖池里的線程拿到對(duì)象鎖后,進(jìn)入就緒狀態(tài)。
運(yùn)行中狀態(tài)(RUNNABLE之RUNNING)
線程調(diào)度程序從可運(yùn)行池中選擇一個(gè)線程作為當(dāng)前線程時(shí)線程所處的狀態(tài)。這也是線程進(jìn)入運(yùn)行狀態(tài)的唯一的一種方式。
阻塞狀態(tài)(BLOCKED)
阻塞狀態(tài)是線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài)。
等待(WAITING)
處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無限期等待的狀態(tài)。
超時(shí)等待(TIMED_WAITING)
處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,不過無須無限期等待被其他線程顯示地喚醒,在達(dá)到一定時(shí)間后它們會(huì)自動(dòng)喚醒。
終止?fàn)顟B(tài)(TERMINATED)
當(dāng)線程的run()方法完成時(shí),或者主線程的main()方法完成時(shí),我們就認(rèn)為它終止了。這個(gè)線程對(duì)象也許是活的,但是它已經(jīng)不是一個(gè)單獨(dú)執(zhí)行的線程。線程一旦終止了,就不能復(fù)生。
在一個(gè)終止的線程上調(diào)用start()方法,會(huì)拋出java.lang.IllegalThreadStateException異常。
1.3. 線程的狀態(tài)轉(zhuǎn)換及控制
主要由這幾個(gè)方法來控制:sleep、join、yield、wait、notify以及notifyAll。
wait() / notify() / notifyAll()
wait(),notify(),notifyAll() 是定義在Object類的實(shí)例方法,用于控制線程狀態(tài),三個(gè)方法都必須在synchronized 同步關(guān)鍵字所限定的作用域中調(diào)用(只能在同步控制方法或者同步控制塊中使用),否則會(huì)報(bào)錯(cuò) java.lang.IllegalMonitorStateException。
join() / sleep() / yield()
join()
如果線程A調(diào)用了線程B的join方法,線程A將被阻塞,等待線程B執(zhí)行完畢后線程A才會(huì)被執(zhí)行。這里需要注意一點(diǎn)的是,join方法必須在線程B的start方法調(diào)用之后調(diào)用才有意義。join方法的主要作用就是實(shí)現(xiàn)線程間的同步,它可以使線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。
sleep()
當(dāng)線程A調(diào)用了 sleep方法,則線程A將被阻塞,直到指定睡眠的時(shí)間到達(dá)后,線程A才會(huì)重新被喚起,進(jìn)入就緒狀態(tài)。
public class Test {public static void main(String[] args) {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "---" + i);try {Thread.sleep(1000); // 阻塞當(dāng)前線程1s} catch (Exception e) {e.printStackTrace();}}} }yield() 當(dāng)線程A調(diào)用了yield方法,它可以暫時(shí)放棄處理器,但是線程A不會(huì)被阻塞,而是進(jìn)入就緒狀態(tài)。執(zhí)行了yield方法的線程什么時(shí)候會(huì)繼續(xù)運(yùn)行由線程調(diào)度器來決定。
public class YieldThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "---" + i);// 主動(dòng)放棄Thread.yield();}} }sleep方法和wait方法的區(qū)別是什么?
wait方法既釋放cpu,又釋放鎖。 sleep方法只釋放cpu,但是不釋放鎖。
sleep 方法是Thread類的一個(gè)靜態(tài)方法,其作用是使運(yùn)行中的線程暫時(shí)停止指定的毫秒數(shù),從而該線程進(jìn)入阻塞狀態(tài)并讓出處理器,將執(zhí)行的機(jī)會(huì)讓給其他線程。但是這個(gè)過程中監(jiān)控狀態(tài)始終保持,當(dāng)sleep的時(shí)間到了之后線程會(huì)自動(dòng)恢復(fù)。
wait 方法是Object類的方法,它是用來實(shí)現(xiàn)線程同步的。當(dāng)調(diào)用某個(gè)對(duì)象的wait方法后,當(dāng)前線程會(huì)被阻塞并釋放同步鎖,直到其他線程調(diào)用了該對(duì)象的 notify 方法或者 notifyAll 方法來喚醒該線程。所以 wait 方法和 notify(或notifyAll)應(yīng)當(dāng)成對(duì)出現(xiàn)以保證線程間的協(xié)調(diào)運(yùn)行。
1.4. Java如何正確停止線程
注意:Java中線程的stop()、suspend()、resume()三個(gè)方法都已經(jīng)被棄用,所以不再使用stop()方法停止線程。
我們只能調(diào)用線程的interrupt()方法通知系統(tǒng)停止線程,并不能強(qiáng)制停止線程。線程能否停止,何時(shí)停止,取決于系統(tǒng)。
1.5 線程池(非常重要)
線程池的地位十分重要,基本上涉及到跨線程的框架都使用到了線程池,比如說OkHttp、RxJava、LiveData以及協(xié)程等。
與新建一個(gè)線程相比,線程池的特點(diǎn)?
ExecutorService簡(jiǎn)介
通常來說我們說到線程池第一時(shí)間想到的就是它:ExecutorService,它是一個(gè)接口,其實(shí)如果要從真正意義上來說,它可以叫做線程池的服務(wù),因?yàn)樗峁┝吮姸嘟涌赼pi來控制線程池中的線程,而真正意義上的線程池就是:ThreadPoolExecutor,它實(shí)現(xiàn)了ExecutorService接口,并封裝了一系列的api使得它具有線程池的特性,其中包括工作隊(duì)列、核心線程數(shù)、最大線程數(shù)等。
線程池(ThreadPoolExecutor)中的幾個(gè)參數(shù)是什么意思?
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {//...}參數(shù)解釋如下(重要):
corePoolSize:核心線程數(shù)量,不會(huì)釋放。
maximumPoolSize:允許使用的最大線程池?cái)?shù)量,非核心線程數(shù)量,閑置時(shí)會(huì)釋放。
keepAliveTime:閑置線程允許的最大閑置時(shí)間。它起作用必須在一個(gè)前提下,就是當(dāng)線程池中的線程數(shù)量超過了corePoolSize時(shí),它表示多余的空閑線程的存活時(shí)間,即:多余的空閑線程在超過keepAliveTime時(shí)間內(nèi)沒有任務(wù)的話則被銷毀。而這個(gè)主要應(yīng)用在緩存線程池中
unit:閑置時(shí)間的單位。
workQueue:阻塞隊(duì)列,用來存儲(chǔ)已經(jīng)提交但未被執(zhí)行的任務(wù),不同的阻塞隊(duì)列有不同的特性。
threadFactory:線程工廠,用來創(chuàng)建線程池中的線程,通常用默認(rèn)的即可
handler:通常叫做拒絕策略,1、在線程池已經(jīng)關(guān)閉的情況下 2、任務(wù)太多導(dǎo)致最大線程數(shù)和任務(wù)隊(duì)列已經(jīng)飽和,無法再接收新的任務(wù) 。在上面兩種情況下,只要滿足其中一種時(shí),在使用execute()來提交新的任務(wù)時(shí)將會(huì)拒絕,而默認(rèn)的拒絕策略是拋一個(gè)RejectedExecutionException異常
上面的參數(shù)理解起來都比較簡(jiǎn)單,不過workQueue這個(gè)任務(wù)隊(duì)列卻要再次說明一下,它是一個(gè)BlockingQueue<Runnable>對(duì)象,而泛型則限定它是用來存放Runnable對(duì)象的,剛剛上面講了,不同的線程池它的任務(wù)隊(duì)列實(shí)現(xiàn)肯定是不一樣的,所以,保證不同線程池有著不同的功能的核心就是這個(gè)workQueue的實(shí)現(xiàn)了,細(xì)心的會(huì)發(fā)現(xiàn)在剛剛的用來創(chuàng)建線程池的工廠方法中,針對(duì)不同的線程池傳入的workQueue也不一樣,五種線程池分別用的是什么BlockingQueue:
1、newFixedThreadPool()—>LinkedBlockingQueue 無界的隊(duì)列 2、newSingleThreadExecutor()—>LinkedBlockingQueue 無界的隊(duì)列 3、newCachedThreadPool()—>SynchronousQueue 直接提交的隊(duì)列 4、newScheduledThreadPool()—>DelayedWorkQueue 等待隊(duì)列 5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue 等待隊(duì)列線程池中用到的三種阻塞隊(duì)列:
LinkedBlockingQueue:無界的隊(duì)列
它的容量是 Integer.MAX_VALUE,為 231 -1 ,是一個(gè)非常大的值,可以認(rèn)為是無界隊(duì)列。FixedThreadPool 和 SingleThreadExecutor 線程池的線程數(shù)是固定的,所以沒有辦法增加特別多的線程來處理任務(wù),這時(shí)就需要 LinkedBlockingQueue 這樣一個(gè)沒有容量限制的阻塞隊(duì)列來存放任務(wù)。
SynchronousQueue:直接提交的隊(duì)列
如果不希望任務(wù)在隊(duì)列中等待而是希望將任務(wù)直接移交給工作線程,可使用SynchronousQueue作為等待隊(duì)列。SynchronousQueue不是一個(gè)真正的隊(duì)列,而是一種線程之間移交的機(jī)制。要將一個(gè)元素放入SynchronousQueue中,必須有另一個(gè)線程正在等待接收這個(gè)元素。只有在使用無界線程池或者有飽和策略時(shí)才建議使用該隊(duì)列。
DelayedWorkQueue:等待隊(duì)列
它對(duì)應(yīng)的線程池分別是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,這兩種線程池的最大特點(diǎn)就是可以延遲執(zhí)行任務(wù),比如說一定時(shí)間后執(zhí)行任務(wù)或是每隔一定的時(shí)間執(zhí)行一次任務(wù)。
DelayedWorkQueue 的特點(diǎn)是內(nèi)部元素并不是按照放入的時(shí)間排序,而是會(huì)按照延遲的時(shí)間長(zhǎng)短對(duì)任務(wù)進(jìn)行排序,內(nèi)部采用的是“堆”的數(shù)據(jù)結(jié)構(gòu)(堆的應(yīng)用之一就是 優(yōu)先級(jí)隊(duì)列)。之所以線程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 選擇 DelayedWorkQueue,是因?yàn)樗鼈儽旧碚腔跁r(shí)間執(zhí)行任務(wù)的,而延遲隊(duì)列正好可以把任務(wù)按時(shí)間進(jìn)行排序,方便任務(wù)的執(zhí)行。
線程池的種類有哪些:五種功能不一樣的線程池:
這樣創(chuàng)建線程池的話,我們需要配置一堆東西,非常麻煩。所以,官方也不推薦使用這種方法來創(chuàng)建線程池,而是推薦使用Executors的工廠方法來創(chuàng)建線程池,Executors類是官方提供的一個(gè)工廠類,它里面封裝好了眾多功能不一樣的線程池(但底層實(shí)現(xiàn)還是通過ThreadPoolExecutor),從而使得我們創(chuàng)建線程池非常的簡(jiǎn)便,主要提供了如下五種功能不一樣的線程池:
newCachedThreadPool() :返回一個(gè)可以根據(jù)實(shí)際情況調(diào)整線程池中線程的數(shù)量的線程池。即該線程池中的線程數(shù)量不確定,是根據(jù)實(shí)際情況動(dòng)態(tài)調(diào)整的。
newFixedThreadPool() :線程池只能存放指定數(shù)量的線程池,線程不會(huì)釋放,可重復(fù)利用。
newSingleThreadExecutor() :單線程的線程池。即每次只能執(zhí)行一個(gè)線程任務(wù),多余的任務(wù)會(huì)保存到一個(gè)任務(wù)隊(duì)列中,等待這一個(gè)線程空閑,當(dāng)這個(gè)線程空閑了再按FIFO方式順序執(zhí)行任務(wù)隊(duì)列中的任務(wù)。
newScheduledThreadPool() :可定時(shí)和重復(fù)執(zhí)行的線程池。
newSingleThreadScheduledExecutor():同上。和上面的區(qū)別是該線程池大小為1,而上面的可以指定線程池的大小。
通過Executors的工廠方法來獲取:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();通過Executors的工廠方法來創(chuàng)建線程池極其簡(jiǎn)便,其實(shí)它的內(nèi)部還是通過new ThreadPoolExecutor(…)的方式創(chuàng)建線程池的,我們看一下這些工廠方法的內(nèi)部實(shí)現(xiàn):
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));}public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}線程池ThreadPoolExecutor的使用
使用線程池,其中涉及到一個(gè)極其重要的方法,即:
execute(Runnable command)該方法意為執(zhí)行給定的任務(wù),該任務(wù)處理可能在新的線程、已入池的線程或者正調(diào)用的線程,這由ThreadPoolExecutor的實(shí)現(xiàn)決定。
五種線程池使用舉例:
newFixedThreadPool 創(chuàng)建一個(gè)固定線程數(shù)量的線程池,示例為:
創(chuàng)建了一個(gè)線程數(shù)為3的固定線程數(shù)量的線程池,同理該線程池支持的線程最大并發(fā)數(shù)也是3,而我模擬了10個(gè)任務(wù)讓它處理,執(zhí)行的情況則是首先執(zhí)行前三個(gè)任務(wù),后面7個(gè)則依次進(jìn)入任務(wù)隊(duì)列進(jìn)行等待,執(zhí)行完前三個(gè)任務(wù)后,再通過FIFO的方式從任務(wù)隊(duì)列中取任務(wù)執(zhí)行,直到最后任務(wù)都執(zhí)行完畢。
newSingleThreadExecutor
創(chuàng)建一個(gè)只有一個(gè)線程的線程池,每次只能執(zhí)行一個(gè)線程任務(wù),多余的任務(wù)會(huì)保存到一個(gè)任務(wù)隊(duì)列中,等待線程處理完再依次處理任務(wù)隊(duì)列中的任務(wù),示例為:
其實(shí)我們通過newSingleThreadExecutor()和newFixedThreadPool()的方法發(fā)現(xiàn),創(chuàng)建一個(gè)singleThreadExecutorPool實(shí)際上就是創(chuàng)建一個(gè)核心線程數(shù)和最大線程數(shù)都為1的fixedThreadPool。
newCachedThreadPool
創(chuàng)建一個(gè)可以根據(jù)實(shí)際情況調(diào)整線程池中線程的數(shù)量的線程池,為了體現(xiàn)該線程池可以自動(dòng)根據(jù)實(shí)現(xiàn)情況進(jìn)行線程的重用,而不是一味的創(chuàng)建新的線程去處理任務(wù),我設(shè)置了每隔1s去提交一個(gè)新任務(wù),這個(gè)新任務(wù)執(zhí)行的時(shí)間也是動(dòng)態(tài)變化的,示例為
newScheduledThreadPool
創(chuàng)建一個(gè)可以定時(shí)或者周期性執(zhí)行任務(wù)的線程池,示例為:
newSingleThreadScheduledExecutor
創(chuàng)建一個(gè)可以定時(shí)或者周期性執(zhí)行任務(wù)的線程池,該線程池的線程數(shù)為1,示例為
這個(gè)和上面的沒什么太大區(qū)別,只不過是線程池內(nèi)線程數(shù)量的不同,效果為:每隔2秒就會(huì)執(zhí)行一次該任務(wù)
自定義線程池ThreadPoolExecutor(自行了解)
線程池的狀態(tài):
RUNNING:線程池一旦被創(chuàng)建,就處于 RUNNING 狀態(tài),任務(wù)數(shù)為 0,能夠接收新任務(wù),對(duì)已排隊(duì)的任務(wù)進(jìn)行處理。
SHUTDOWN:不接收新任務(wù),但能處理已排隊(duì)的任務(wù)。調(diào)用線程池的 shutdown() 方法,線程池由 RUNNING 轉(zhuǎn)變?yōu)?SHUTDOWN 狀態(tài)。
STOP:不接收新任務(wù),不處理已排隊(duì)的任務(wù),并且會(huì)中斷正在處理的任務(wù)。調(diào)用線程池的 shutdownNow() 方法,線程池由(RUNNING 或 SHUTDOWN ) 轉(zhuǎn)變?yōu)?STOP 狀態(tài)。
TIDYING:SHUTDOWN 狀態(tài)下,任務(wù)數(shù)為 0, 其他所有任務(wù)已終止,線程池會(huì)變?yōu)?TIDYING 狀態(tài),會(huì)執(zhí)行 terminated() 方法。線程池中的 terminated() 方法是空實(shí)現(xiàn),可以重寫該方法進(jìn)行相應(yīng)的處理。
線程池在 SHUTDOWN 狀態(tài),任務(wù)隊(duì)列為空且執(zhí)行中任務(wù)為空,線程池就會(huì)由 SHUTDOWN 轉(zhuǎn)變?yōu)?TIDYING 狀態(tài)。
線程池在 STOP 狀態(tài),線程池中執(zhí)行中任務(wù)為空時(shí),就會(huì)由 STOP 轉(zhuǎn)變?yōu)?TIDYING 狀態(tài)。
TERMINATED:線程池徹底終止。線程池在 TIDYING 狀態(tài)執(zhí)行完 terminated() 方法就會(huì)由 TIDYING 轉(zhuǎn)變?yōu)?TERMINATED 狀態(tài)。
線程池的停止
關(guān)于線程池的停止,ExecutorService為我們提供了兩個(gè)方法:shutdown和shutdownNow,這兩個(gè)方法各有不同,可以根據(jù)實(shí)際需求方便的運(yùn)用,如下:
線程池的工作流程
簡(jiǎn)單說:
- 任務(wù)來了,優(yōu)先考慮核心線程。
- 核心線程滿了,進(jìn)入阻塞隊(duì)列。
- 阻塞隊(duì)列滿了,考慮非核心線程。
- 非核心線程滿了,再觸發(fā)拒絕任務(wù)。
詳細(xì)說明:
1 當(dāng)一個(gè)任務(wù)通過submit或者execute方法提交到線程池的時(shí)候,如果當(dāng)前池中線程數(shù)(包括閑置線程)小于coolPoolSize,則創(chuàng)建一個(gè)線程執(zhí)行該任務(wù)。
2 如果當(dāng)前線程池中線程數(shù)已經(jīng)達(dá)到coolPoolSize,則將任務(wù)放入等待隊(duì)列。
3 如果任務(wù)不能入隊(duì),說明等待隊(duì)列已滿,若當(dāng)前池中線程數(shù)小于maximumPoolSize,則創(chuàng)建一個(gè)臨時(shí)線程(非核心線程)執(zhí)行該任務(wù)。
4 如果當(dāng)前池中線程數(shù)已經(jīng)等于maximumPoolSize,此時(shí)無法執(zhí)行該任務(wù),根據(jù)拒絕執(zhí)行策略處理。
注意:當(dāng)池中線程數(shù)大于coolPoolSize,超過keepAliveTime時(shí)間的閑置線程會(huì)被回收掉?;厥盏氖欠呛诵木€程,核心線程一般是不會(huì)回收的。如果設(shè)置allowCoreThreadTimeOut(true),則核心線程在閑置keepAliveTime時(shí)間后也會(huì)被回收。
任務(wù)隊(duì)列是一個(gè)阻塞隊(duì)列,線程執(zhí)行完任務(wù)后會(huì)去隊(duì)列取任務(wù)來執(zhí)行,如果隊(duì)列為空,線程就會(huì)阻塞,直到取到任務(wù)。
其它面試題:
1.當(dāng)線程池的核心線程數(shù)量過大或者過小的影響?
首先,多線程編程中一般線程的個(gè)數(shù)都大于CPU核心的個(gè)數(shù),而一個(gè)CPU核心在任意時(shí)刻只能被一個(gè)線程使用,為了讓這些線程都能得到有效的執(zhí)行,CPU采取的策略是為了每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓其他線程使用,這個(gè)過程就屬于一次上下文切換。
當(dāng)線程池中核心線程數(shù)量過大時(shí),線程與線程之間會(huì)爭(zhēng)取CPU資源,這樣就會(huì)導(dǎo)致上下文切換。過多的上下文切換會(huì)增加線程的執(zhí)行時(shí)間,影響了整體執(zhí)行的效率;
當(dāng)線程池中的核心線程數(shù)量過少時(shí),如果同一時(shí)間有大量任務(wù)需要處理,可能會(huì)導(dǎo)致大量任務(wù)在任務(wù)隊(duì)列中排隊(duì),甚至?xí)霈F(xiàn)隊(duì)列滿了之后任務(wù)無法執(zhí)行的情況,或者大量任務(wù)堆積在任務(wù)隊(duì)列導(dǎo)致內(nèi)存溢出(OOM)。
2.CPU密集型和IO密集型?
CPU密集型:比如加密、解密、壓縮、計(jì)算等一系列需要大量耗費(fèi) CPU 資源的任務(wù)。CPU 密集型任務(wù)應(yīng)配置盡可能小的線程,如配置CPU核數(shù) + 1個(gè)線程的線程池。如果設(shè)置過多的線程數(shù),假設(shè)設(shè)置的線程數(shù)是 CPU 核心數(shù)的 2 倍以上,因?yàn)橛?jì)算任務(wù)非常重,會(huì)占用大量的 CPU 資源,所以這時(shí) CPU 的每個(gè)核心工作基本都是滿負(fù)荷的,而我們又設(shè)置了過多的線程,每個(gè)線程都想去利用 CPU 資源來執(zhí)行自己的任務(wù),這就會(huì)造成不必要的上下文切換,此時(shí)線程數(shù)的增多并沒有讓性能提升,反而由于線程數(shù)量過多會(huì)導(dǎo)致性能下降。
I/O密集型:比如數(shù)據(jù)庫、文件的讀寫,網(wǎng)絡(luò)通信等任務(wù),這種任務(wù)的特點(diǎn)是并不會(huì)特別消耗 CPU 資源,但是 IO 操作很耗時(shí),總體會(huì)占用比較多的時(shí)間。由于 IO 密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如CPU核數(shù) * 2。因?yàn)?IO 讀寫速度相比于 CPU 的速度而言是比較慢的,如果我們?cè)O(shè)置過少的線程數(shù),就可能導(dǎo)致 CPU 資源的浪費(fèi)。而如果我們?cè)O(shè)置更多的線程數(shù),那么當(dāng)一部分線程正在等待 IO 的時(shí)候,它們此時(shí)并不需要 CPU 來計(jì)算,那么另外的線程便可以利用 CPU 去執(zhí)行其他的任務(wù),互不影響,這樣的話在工作隊(duì)列中等待的任務(wù)就會(huì)減少,可以更好地利用資源。
混合型任務(wù):既包含CPU密集型又包含I/O密集型。
3.corePoolSize核心線程數(shù),一般設(shè)置為多少?
首先要考慮到 CPU 核心數(shù);可以使用下面的方法獲取
Runtime.getRuntime().availableProcessor() 方法來獲取(可能不準(zhǔn)確,作為參考)在確認(rèn)了核心數(shù)后,再去判斷是 CPU 密集型任務(wù)還是 IO 密集型任務(wù):
- CPU密集型:核心線程數(shù) = CPU核數(shù) + 1
- IO密集型:核心線程數(shù) = CPU核數(shù) * 2
注:IO密集型(某大廠實(shí)踐經(jīng)驗(yàn))
核心線程數(shù) = CPU核數(shù) / (1-阻塞系數(shù))
其中計(jì)算密集型阻塞系數(shù)為 0,IO 密集型阻塞系數(shù)接近 1,一般認(rèn)為在 0.8 ~ 0.9 之間。比如 8 核
CPU,按照公式就是 2 / ( 1 - 0.9 ) = 20 個(gè)線程數(shù)
另外,可參考AsyncTask源碼中的設(shè)置:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();// We want at least 2 threads and at most 4 threads in the core pool,// preferring to have 1 less than the CPU count to avoid saturating// the CPU with background workprivate static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;源碼有解釋的:之所以 減掉這個(gè)1,是因?yàn)闉榱吮苊夂笈_(tái)任務(wù)將 CPU 資源完全耗盡, 減掉的這個(gè)1 是留給我們 主線程 使用的。
1.6. Java鎖機(jī)制
在java中,解決同步問題,很多時(shí)候都會(huì)使用到synchronized和Lock,這兩者都是在多線程并發(fā)時(shí)候常使用的鎖機(jī)制。在JDK1.6后,對(duì)synchronized進(jìn)行了很多優(yōu)化,如偏向鎖、輕量級(jí)鎖等,synchronized的性能已經(jīng)與Reentrantlock大致相同,除非要使用Reentrantlock的一些高級(jí)功能(實(shí)現(xiàn)公平鎖、中斷鎖等),一般推薦使用synchronized關(guān)鍵字來實(shí)現(xiàn)加鎖機(jī)制。
Synchronized 是Java 并發(fā)編程中很重要的關(guān)鍵字,另外一個(gè)很重要的是 volatile。Syncronized 一次只允許一個(gè)線程進(jìn)入由他修飾的代碼段,從而允許他們進(jìn)行自我保護(hù)。進(jìn)入由Synchronized 保護(hù)的代碼區(qū)首先需要獲取 Synchronized 這把鎖,其他線程想要執(zhí)行必須進(jìn)行等待。Synchronized 鎖住的代碼區(qū)域執(zhí)行完成后需要把鎖歸還,也就是釋放鎖,這樣才能夠讓其他線程使用。
Lock 是 Java并發(fā)編程中很重要的一個(gè)接口,它要比 Synchronized 關(guān)鍵字更能直譯"鎖"的概念,Lock需要手動(dòng)加鎖和手動(dòng)解鎖,一般通過 lock.lock() 方法來進(jìn)行加鎖, 通過 lock.unlock() 方法進(jìn)行解鎖。與 Lock 關(guān)聯(lián)密切的鎖有 ReetrantLock 和 ReadWriteLock。
ReetrantLock 實(shí)現(xiàn)了Lock接口,它是一個(gè)可重入鎖,內(nèi)部定義了公平鎖與非公平鎖。
ReadWriteLock 一個(gè)用來獲取讀鎖,一個(gè)用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個(gè)鎖來分配給線程,從而使得多個(gè)線程可以同時(shí)進(jìn)行讀操作。ReentrantReadWirteLock實(shí)現(xiàn)了ReadWirteLock接口,并未實(shí)現(xiàn)Lock接口。
Synchronized 的使用
修飾一個(gè)方法:即一次只能有一個(gè)線程進(jìn)入該方法,其他線程要想在此時(shí)調(diào)用該方法,只能排隊(duì)等候。
實(shí)例方法:鎖住的是該類的實(shí)例對(duì)象 靜態(tài)方法:鎖住的是該類的類對(duì)象。public synchronized void goHOme(){ }public static synchronized void goHOme(){ }修飾代碼塊:表示只能有一個(gè)線程進(jìn)入某個(gè)代碼段
public void numDecrease(Object num){synchronized (num){number++;} }修飾一個(gè)類:作用的對(duì)象是這個(gè)類的所有對(duì)象,只要是這個(gè)類型的class不管有幾個(gè)對(duì)象都會(huì)起作用。
class Person {public void method() {//鎖住的是該類的類對(duì)象,如果換成this或其他object,則鎖住的是該類的實(shí)例對(duì)象synchronized(Person.class) {// todo}}}獲取對(duì)象鎖
synchronized(this|object) {} 修飾非靜態(tài)方法獲取類鎖
synchronized(類.class) {} 修飾靜態(tài)方法Lock 的使用
public interface Lock {void lock();void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
使用示例:
一般來說,使用Lock必須在try{}catch{}塊中進(jìn)行,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被被釋放,防止死鎖的發(fā)生。
注意,當(dāng)一個(gè)線程獲取了鎖之后,是不會(huì)被interrupt()方法中斷的。單獨(dú)調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程。因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí),如果不能獲取到,只有進(jìn)行等待的情況下,是可以響應(yīng)中斷的。而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài),是無法被中斷的,只有一直等待下去。
synchronized和Lock的區(qū)別?
主要區(qū)別:
synchronized是Java中的關(guān)鍵字,是Java的內(nèi)置實(shí)現(xiàn);Lock是Java中的接口。 synchronized遇到異常會(huì)釋放鎖;Lock需要在發(fā)生異常的時(shí)候調(diào)用成員方法Lock#unlock()方法。 synchronized是不可以中斷的,Lock可中斷。 synchronized不能去嘗試獲得鎖,沒有獲得鎖就會(huì)被阻塞; Lock可以去嘗試獲得鎖,如果未獲得可以嘗試處理其他邏輯。 synchronized多線程效率不如Lock,不過Java在1.6以后已經(jīng)對(duì)synchronized進(jìn)行大量的優(yōu)化,所以性能上來講,其實(shí)差不了多少。死鎖
所謂死鎖是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過程中,因爭(zhēng)奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。
死鎖觸發(fā)的四大條件?
互斥鎖 請(qǐng)求與保持 不可剝奪 循環(huán)的請(qǐng)求與等待簡(jiǎn)單死鎖代碼示例:
public class DeadLock {public static String obj1 = "obj1";public static String obj2 = "obj2";public static void main(String[] args){Thread a = new Thread(new Lock1());Thread b = new Thread(new Lock2());a.start();b.start();} } class Lock1 implements Runnable{@Overridepublic void run(){try{System.out.println("Lock1 running");while(true){synchronized(DeadLock.obj1){System.out.println("Lock1 lock obj1");Thread.sleep(3000);//獲取obj1后先等一會(huì)兒,讓Lock2有足夠的時(shí)間鎖住obj2synchronized(DeadLock.obj2){System.out.println("Lock1 lock obj2");}}}}catch(Exception e){e.printStackTrace();}} } class Lock2 implements Runnable{@Overridepublic void run(){try{System.out.println("Lock2 running");while(true){synchronized(DeadLock.obj2){System.out.println("Lock2 lock obj2");Thread.sleep(3000);synchronized(DeadLock.obj1){System.out.println("Lock2 lock obj1");}}}}catch(Exception e){e.printStackTrace();}} }
可以看到,Lock1獲取obj1,Lock2獲取obj2,但是它們都沒有辦法再獲取另外一個(gè)obj,因?yàn)樗鼈兌荚诘却龑?duì)方先釋放鎖,這時(shí)就是死鎖。
1.7. Java中的主流鎖
Java中往往是按照是否含有某一特性來定義鎖,我們通過特性將鎖進(jìn)行分組歸類:
1.樂觀鎖 VS 悲觀鎖
概念:對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,樂觀鎖認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù),所以不會(huì)添加鎖,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒有被更新,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或者自動(dòng)重試)。
悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改。Java中,synchronized關(guān)鍵字和Lock的實(shí)現(xiàn)類都是悲觀鎖。
樂觀鎖在Java中是通過使用無鎖編程來實(shí)現(xiàn),最常采用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實(shí)現(xiàn)的。
根據(jù)從上面的概念描述我們可以發(fā)現(xiàn):
- 悲觀鎖適合寫操作多的場(chǎng)景,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
- 樂觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。
調(diào)用方式示例:
悲觀鎖基本都是在顯式的鎖定之后再操作同步資源,而樂觀鎖則直接去操作同步資源。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實(shí)現(xiàn)線程同步呢?
樂觀鎖的主要實(shí)現(xiàn)方式 “CAS” 的技術(shù)原理:
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實(shí)現(xiàn)了樂觀鎖。
CAS算法涉及到三個(gè)操作數(shù):
- 需要讀寫的內(nèi)存值 V。
- 進(jìn)行比較的值 A。
- 要寫入的新值 B。
當(dāng)且僅當(dāng) V 的值等于 A 時(shí),CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個(gè)原子操作),否則不會(huì)執(zhí)行任何操作。一般情況下,“更新”是一個(gè)不斷重試的操作。
之前提到j(luò)ava.util.concurrent包中的原子類,就是通過CAS來實(shí)現(xiàn)了樂觀鎖,那么我們進(jìn)入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:
根據(jù)定義我們可以看出各屬性的作用:
- unsafe: 獲取并操作內(nèi)存的數(shù)據(jù)。
- valueOffset: 存儲(chǔ)value在AtomicInteger中的偏移量。
- value: 存儲(chǔ)AtomicInteger的int值,該屬性需要借助volatile關(guān)鍵字保證其在線程間是可見的。
接下來,我們查看AtomicInteger的自增函數(shù)incrementAndGet()的源碼時(shí),發(fā)現(xiàn)自增函數(shù)底層調(diào)用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通過class文件中的參數(shù)名,并不能很好的了解方法的作用,所以我們通過OpenJDK 8 來查看Unsafe的源碼:
根據(jù)OpenJDK 8的源碼我們可以看出,getAndAddInt()循環(huán)獲取給定對(duì)象o中的偏移量處的值v,然后判斷內(nèi)存值是否等于v。如果相等則將內(nèi)存值設(shè)置為 v + delta,否則返回false,繼續(xù)循環(huán)進(jìn)行重試,直到設(shè)置成功才能退出循環(huán),并且將舊值返回。整個(gè)“比較+更新”操作封裝在compareAndSwapInt()中,在JNI里是借助于一個(gè)CPU指令完成的,屬于原子操作,可以保證多個(gè)線程都能夠看到同一個(gè)變量的修改值。
后續(xù)JDK通過CPU的cmpxchg指令,去比較寄存器中的 A 和 內(nèi)存中的值 V。如果相等,就把要寫入的新值 B 存入內(nèi)存中。如果不相等,就將內(nèi)存值 V 賦值給寄存器中的值 A。然后通過Java代碼中的while循環(huán)再次調(diào)用cmpxchg指令進(jìn)行重試,直到設(shè)置成功為止。
CAS雖然很高效,但是它也存在三大問題,這里也簡(jiǎn)單說一下:
ABA問題。CAS需要在操作值的時(shí)候檢查內(nèi)存值是否發(fā)生變化,沒有發(fā)生變化才會(huì)更新內(nèi)存值。但是如果內(nèi)存值原來是A,后來變成了B,然后又變成了A,那么CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)值沒有發(fā)生變化,但是實(shí)際上是有變化的。ABA問題的解決思路就是在變量前面添加版本號(hào),每次變量更新的時(shí)候都把版本號(hào)加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等,如果都相等,則以原子方式將引用值和標(biāo)志的值設(shè)置為給定的更新值。
循環(huán)時(shí)間長(zhǎng)開銷大。CAS操作如果長(zhǎng)時(shí)間不成功,會(huì)導(dǎo)致其一直自旋,給CPU帶來非常大的開銷。
只能保證一個(gè)共享變量的原子操作。對(duì)一個(gè)共享變量執(zhí)行操作時(shí),CAS能夠保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),CAS是無法保證操作的原子性的。
Java從1.5開始JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性,可以把多個(gè)變量放在一個(gè)對(duì)象里來進(jìn)行CAS操作。
2.自旋鎖 VS 適應(yīng)性自旋鎖
概念解析:
阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間。如果同步代碼塊中的內(nèi)容過于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)。
在許多場(chǎng)景中,同步資源的鎖定時(shí)間很短,為了這一小段時(shí)間去切換線程,線程掛起和恢復(fù)現(xiàn)場(chǎng)的花費(fèi)可能會(huì)讓系統(tǒng)得不償失。如果物理機(jī)器有多個(gè)處理器,能夠讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面那個(gè)請(qǐng)求鎖的線程不放棄CPU的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。
而為了讓當(dāng)前線程“稍等一下”,我們需讓當(dāng)前線程進(jìn)行自旋,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
自旋鎖本身是有缺點(diǎn)的,它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要占用處理器時(shí)間。如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好。反之,如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白浪費(fèi)處理器資源。所以,自旋等待的時(shí)間必須要有一定的限度,如果自旋超過了限定次數(shù)(默認(rèn)是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應(yīng)當(dāng)掛起線程。
自旋鎖的實(shí)現(xiàn)原理同樣也是CAS,AtomicInteger中調(diào)用unsafe進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個(gè)自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變?yōu)槟J(rèn)開啟,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)。
自適應(yīng)意味著自旋的時(shí)間(次數(shù))不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間。如果對(duì)于某個(gè)鎖,自旋很少成功獲得過,那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,直接阻塞線程,避免浪費(fèi)處理器資源。
在自旋鎖中另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,本文中僅做名詞介紹,不做深入講解,感興趣的同學(xué)可以自行查閱相關(guān)資料。
3.無鎖 VS 偏向鎖 VS 輕量級(jí)鎖 VS 重量級(jí)鎖
這四種鎖是指鎖的狀態(tài),專門針對(duì)synchronized的。在介紹這四種鎖狀態(tài)之前還需要介紹一些額外的知識(shí)。
首先為什么Synchronized能實(shí)現(xiàn)線程同步?
在回答這個(gè)問題之前我們需要了解兩個(gè)重要的概念:“Java對(duì)象頭”、“Monitor”。
Java對(duì)象頭
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對(duì)象頭里的,而Java對(duì)象頭又是什么呢?
我們以Hotspot虛擬機(jī)為例,Hotspot的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)。
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。
Klass Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
Monitor
Monitor可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象。每一個(gè)Java對(duì)象就有一把看不見的鎖,稱為內(nèi)部鎖或者M(jìn)onitor鎖。
Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程都有一個(gè)可用monitor record列表,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè)monitor關(guān)聯(lián),同時(shí)monitor中有一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí),表示該鎖被這個(gè)線程占用。
現(xiàn)在話題回到synchronized,synchronized通過Monitor來實(shí)現(xiàn)線程同步,Monitor是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實(shí)現(xiàn)的線程同步。
如同我們?cè)谧孕i中提到的“阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間。如果同步代碼塊中的內(nèi)容過于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)”。這種方式就是synchronized最初實(shí)現(xiàn)同步的方式,這就是JDK 6之前synchronized效率低的原因。這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”。
所以目前鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。鎖狀態(tài)只能升級(jí)不能降級(jí)。
通過上面的介紹,我們對(duì)synchronized的加鎖機(jī)制以及相關(guān)知識(shí)有了一個(gè)了解,那么下面我們給出四種鎖狀態(tài)對(duì)應(yīng)的的Mark Word內(nèi)容,然后再分別講解四種鎖狀態(tài)的思路以及特點(diǎn):
無鎖
無鎖沒有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功。
無鎖的特點(diǎn)就是修改操作在循環(huán)內(nèi)進(jìn)行,線程會(huì)不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功。上面我們介紹的CAS原理及應(yīng)用即是無鎖的實(shí)現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場(chǎng)合下的性能是非常高的。
偏向鎖
偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問,那么該線程會(huì)自動(dòng)獲取鎖,降低獲取鎖的代價(jià)。
在大多數(shù)情況下,鎖總是由同一線程多次獲得,不存在多線程競(jìng)爭(zhēng),所以出現(xiàn)了偏向鎖。其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí)能夠提高性能。
當(dāng)一個(gè)線程訪問同步代碼塊并獲取鎖時(shí),會(huì)在Mark Word里存儲(chǔ)鎖偏向的線程ID。在線程進(jìn)入和退出同步塊時(shí)不再通過CAS操作來加鎖和解鎖,而是檢測(cè)Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。引入偏向鎖是為了在無多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時(shí)候依賴一次CAS原子指令即可。
偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程不會(huì)主動(dòng)釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài)。撤銷偏向鎖后恢復(fù)到無鎖(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)。
偏向鎖在JDK 6及以后的JVM里是默認(rèn)啟用的。可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
輕量級(jí)鎖
是指當(dāng)鎖是偏向鎖的時(shí)候,被另外的線程所訪問,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。
在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝,然后拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中。
拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word。
如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
如果輕量級(jí)鎖的更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明多個(gè)線程競(jìng)爭(zhēng)鎖。
若當(dāng)前只有一個(gè)等待線程,則該線程通過自旋進(jìn)行等待。但是當(dāng)自旋超過一定的次數(shù),或者一個(gè)線程在持有鎖,一個(gè)在自旋,又有第三個(gè)來訪時(shí),輕量級(jí)鎖升級(jí)為重量級(jí)鎖。
重量級(jí)鎖
升級(jí)為重量級(jí)鎖時(shí),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)。
整體的鎖狀態(tài)升級(jí)流程如下:
綜上,偏向鎖通過對(duì)比Mark Word解決加鎖問題,避免執(zhí)行CAS操作。而輕量級(jí)鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞。
4. 公平鎖 VS 非公平鎖
公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖。公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死,缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景。非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死,或者等很久才會(huì)獲得鎖。
直接用語言描述可能有點(diǎn)抽象,這里用一個(gè)例子來講述一下公平鎖和非公平鎖。
如上圖所示,假設(shè)有一口水井,有管理員看守,管理員有一把鎖,只有拿到鎖的人才能夠打水,打完水要把鎖還給管理員。每個(gè)過來打水的人都要管理員的允許并拿到鎖之后才能去打水,如果前面有人正在打水,那么這個(gè)想要打水的人就必須排隊(duì)。管理員會(huì)查看下一個(gè)要去打水的人是不是隊(duì)伍里排最前面的人,如果是的話,才會(huì)給你鎖讓你去打水;如果你不是排第一的人,就必須去隊(duì)尾排隊(duì),這就是公平鎖。
但是對(duì)于非公平鎖,管理員對(duì)打水的人沒有要求。即使等待隊(duì)伍里有排隊(duì)等待的人,但如果在上一個(gè)人剛打完水把鎖還給管理員而且管理員還沒有允許等待隊(duì)伍里下一個(gè)人去打水時(shí),剛好來了一個(gè)插隊(duì)的人,這個(gè)插隊(duì)的人是可以直接從管理員那里拿到鎖去打水,不需要排隊(duì),原本排隊(duì)等待的人只能繼續(xù)等待。如下圖所示
接下來我們通過ReentrantLock的源碼來講解公平鎖和非公平鎖。
根據(jù)代碼可知,ReentrantLock里面有一個(gè)內(nèi)部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer),添加鎖和釋放鎖的大部分操作實(shí)際上都是在Sync中實(shí)現(xiàn)的。它有公平鎖FairSync和非公平鎖NonfairSync兩個(gè)子類。ReentrantLock默認(rèn)使用非公平鎖,也可以通過構(gòu)造器來顯示的指定使用公平鎖。
下面我們來看一下公平鎖與非公平鎖的加鎖方法的源碼:
通過上圖中的源代碼對(duì)比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時(shí)多了一個(gè)限制條件:hasQueuedPredecessors()。
再進(jìn)入hasQueuedPredecessors(),可以看到該方法主要做一件事情:主要是判斷當(dāng)前線程是否位于同步隊(duì)列中的第一個(gè)。如果是則返回true,否則返回false。
綜上,公平鎖就是通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,從而實(shí)現(xiàn)公平的特性。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖,所以存在后申請(qǐng)卻先獲得鎖的情況。
5. 可重入鎖 VS 非可重入鎖
可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class),不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖。下面用示例代碼來進(jìn)行分析:
在上面的代碼中,類中的兩個(gè)方法都是被內(nèi)置鎖synchronized修飾的,doSomething()方法中調(diào)用doOthers()方法。因?yàn)閮?nèi)置鎖是可重入的,所以同一個(gè)線程在調(diào)用doOthers()時(shí)可以直接獲得當(dāng)前對(duì)象的鎖,進(jìn)入doOthers()進(jìn)行操作。
如果是一個(gè)不可重入鎖,那么當(dāng)前線程在調(diào)用doOthers()之前需要將執(zhí)行doSomething()時(shí)獲取當(dāng)前對(duì)象的鎖釋放掉,實(shí)際上該對(duì)象鎖已被當(dāng)前線程所持有,且無法釋放。所以此時(shí)會(huì)出現(xiàn)死鎖。
而為什么可重入鎖就可以在嵌套調(diào)用時(shí)可以自動(dòng)獲得鎖呢?我們通過圖示和源碼來分別解析一下。
還是打水的例子,有多個(gè)人在排隊(duì)打水,此時(shí)管理員允許鎖和同一個(gè)人的多個(gè)水桶綁定。這個(gè)人用多個(gè)水桶打水時(shí),第一個(gè)水桶和鎖綁定并打完水之后,第二個(gè)水桶也可以直接和鎖綁定并開始打水,所有的水桶都打完水之后打水人才會(huì)將鎖還給管理員。這個(gè)人的所有打水流程都能夠成功執(zhí)行,后續(xù)等待的人也能夠打到水。這就是可重入鎖。
但如果是非可重入鎖的話,此時(shí)管理員只允許鎖和同一個(gè)人的一個(gè)水桶綁定。第一個(gè)水桶和鎖綁定打完水之后并不會(huì)釋放鎖,導(dǎo)致第二個(gè)水桶不能和鎖綁定也無法打水。當(dāng)前線程出現(xiàn)死鎖,整個(gè)等待隊(duì)列中的所有線程都無法被喚醒。
之前我們說過ReentrantLock和synchronized都是重入鎖,那么我們通過重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來對(duì)比分析一下為什么非可重入鎖在重復(fù)調(diào)用同步資源時(shí)會(huì)出現(xiàn)死鎖。
首先ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護(hù)了一個(gè)同步狀態(tài)status來計(jì)數(shù)重入次數(shù),status初始值為0。
當(dāng)線程嘗試獲取鎖時(shí),可重入鎖先嘗試獲取并更新status值,如果status == 0表示沒有其他線程在執(zhí)行同步代碼,則把status置為1,當(dāng)前線程開始執(zhí)行。如果status != 0,則判斷當(dāng)前線程是否是獲取到這個(gè)鎖的線程,如果是的話執(zhí)行status+1,且當(dāng)前線程可以再次獲取鎖。而非可重入鎖是直接去獲取并嘗試更新當(dāng)前status的值,如果status != 0的話會(huì)導(dǎo)致其獲取鎖失敗,當(dāng)前線程阻塞。
釋放鎖時(shí),可重入鎖同樣先獲取當(dāng)前status的值,在當(dāng)前線程是持有鎖的線程的前提下。如果status-1 == 0,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會(huì)真正釋放鎖。而非可重入鎖則是在確定當(dāng)前線程是持有鎖的線程之后,直接將status置為0,將鎖釋放。
6. 獨(dú)享鎖 VS 共享鎖
獨(dú)享鎖和共享鎖同樣是一種概念。我們先介紹一下具體的概念,然后通過ReentrantLock和ReentrantReadWriteLock的源碼來介紹獨(dú)享鎖和共享鎖。
獨(dú)享鎖也叫排他鎖,是指該鎖一次只能被一個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上排它鎖后,則其他線程不能再對(duì)A加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。JDK中的synchronized和JUC中Lock的實(shí)現(xiàn)類就是互斥鎖。
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后,則其他線程只能對(duì)A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。
獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享。
下圖為ReentrantReadWriteLock的部分源碼:
我們看到ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock,由詞知意,一個(gè)讀鎖一個(gè)寫鎖,合稱“讀寫鎖”。再進(jìn)一步觀察可以發(fā)現(xiàn)ReadLock和WriteLock是靠?jī)?nèi)部類Sync實(shí)現(xiàn)的鎖。Sync是AQS的一個(gè)子類,這種結(jié)構(gòu)在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在ReentrantReadWriteLock里面,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨(dú)享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因?yàn)樽x鎖和寫鎖是分離的。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。
那讀鎖和寫鎖的具體加鎖方式有什么區(qū)別呢?在了解源碼之前我們需要回顧一下其他知識(shí)。
在最開始提及AQS的時(shí)候我們也提到了state字段(int類型,32位),該字段用來描述有多少線程獲持有鎖。
在獨(dú)享鎖中這個(gè)值通常是0或者1(如果是重入鎖的話state值就是重入的次數(shù)),在共享鎖中state就是持有鎖的數(shù)量。但是在ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要在一個(gè)整型變量state上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))。于是將state變量“按位切割”切分成了兩個(gè)部分,高16位表示讀鎖狀態(tài)(讀鎖個(gè)數(shù)),低16位表示寫鎖狀態(tài)(寫鎖個(gè)數(shù))。如下圖所示:
了解了概念之后我們?cè)賮砜创a,先看寫鎖的加鎖源碼:
- 這段代碼首先取到當(dāng)前鎖的個(gè)數(shù)c,然后再通過c來獲取寫鎖的個(gè)數(shù)w。因?yàn)閷戞i是低16位,所以取低16位的最大值與當(dāng)前的c做與運(yùn)算( int w
= exclusiveCount; ),高16位和0與運(yùn)算后是0,剩下的就是低位運(yùn)算的值,同時(shí)也是持有寫鎖的線程數(shù)目。 - 在取到寫鎖線程的數(shù)目后,首先判斷是否已經(jīng)有線程持有了鎖。如果已經(jīng)有線程持有了鎖(c!=0),則查看當(dāng)前寫鎖線程的數(shù)目,如果寫線程數(shù)為0(即此時(shí)存在讀鎖)或者持有鎖的線程不是當(dāng)前線程就返回失敗(涉及到公平鎖和非公平鎖的實(shí)現(xiàn))。
- 如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個(gè)Error。
- 如果當(dāng)且寫線程數(shù)為0(那么讀線程也應(yīng)該為0,因?yàn)樯厦嬉呀?jīng)處理c!=0的情況),并且當(dāng)前線程需要阻塞那么就返回失敗;如果通過CAS增加寫線程數(shù)失敗也返回失敗。
- 如果c=0,w=0或者c>0,w>0(重入),則設(shè)置當(dāng)前線程或鎖的擁有者,返回成功!
tryAcquire()除了重入條件(當(dāng)前線程為獲取了寫鎖的線程)之外,增加了一個(gè)讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在于:必須確保寫鎖的操作對(duì)讀鎖可見,如果允許讀鎖在已被獲取的情況下對(duì)寫鎖的獲取,那么正在運(yùn)行的其他讀線程就無法感知到當(dāng)前寫線程的操作。
因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當(dāng)前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態(tài),當(dāng)寫狀態(tài)為0時(shí)表示寫鎖已被釋放,然后等待的讀寫線程才能夠繼續(xù)訪問讀寫鎖,同時(shí)前次寫線程的修改對(duì)后續(xù)的讀寫線程可見。
接著是讀鎖的代碼:
可以看到在tryAcquireShared(int unused)方法中,如果其他線程已經(jīng)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗,進(jìn)入等待狀態(tài)。如果當(dāng)前線程獲取了寫鎖或者寫鎖未被獲取,則當(dāng)前線程(線程安全,依靠CAS保證)增加讀狀態(tài),成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個(gè)讀線程同時(shí)釋放讀鎖)均減少讀狀態(tài),減少的值是“1<<16”。所以讀寫鎖才能實(shí)現(xiàn)讀讀的過程共享,而讀寫、寫讀、寫寫的過程互斥。
此時(shí),我們?cè)倩仡^看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:
我們發(fā)現(xiàn)在ReentrantLock雖然有公平鎖和非公平鎖兩種,但是它們添加的都是獨(dú)享鎖。根據(jù)源碼所示,當(dāng)某一個(gè)線程調(diào)用lock方法獲取鎖時(shí),如果同步資源沒有被其他線程鎖住,那么當(dāng)前線程在使用CAS更新state成功后就會(huì)成功搶占該資源。而如果公共資源被占用且不是被當(dāng)前線程占用,那么就會(huì)加鎖失敗。所以可以確定ReentrantLock無論讀操作還是寫操作,添加的鎖都是都是獨(dú)享鎖。
1.8. Java中Volatile關(guān)鍵字(重要)
基本概念:Java 內(nèi)存模型中的可見性、原子性和有序性。
原子性:(原子是世界上的最小單位,具有不可分割性)原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時(shí)刻只能有一個(gè)線程來對(duì)它進(jìn)行操作。簡(jiǎn)而言之,在整個(gè)操作過程中不會(huì)被線程調(diào)度器中斷的操作,都可認(rèn)為是原子性。比如 a = 1;
非原子性:
也就是整個(gè)過程中會(huì)出現(xiàn)線程調(diào)度器中斷操作的現(xiàn)象
類似"a ++"這樣的操作不具有原子性,因?yàn)樗赡芤?jīng)過以下兩個(gè)步驟:
(1)取出 a 的值
(2)計(jì)算 a+1
如果有兩個(gè)線程t1,t2都在進(jìn)行這樣的操作。t1在第一步做完之后還沒來得及加1操作就被線程調(diào)度器中斷了,于是t2開始執(zhí)行,t2執(zhí)行完畢后t1開始執(zhí)行第二步(此時(shí)t1中a的值可能還是舊值,不是一定的,只有線程t2中a的值沒有及時(shí)更新到t1中才會(huì)出現(xiàn))。這個(gè)時(shí)候就出現(xiàn)了錯(cuò)誤,t2的操作相當(dāng)于被忽略了
類似于a += 1這樣的操作都不具有原子性。還有一種特殊情況,就是long跟double類型某些情況也不具有原子性
只有簡(jiǎn)單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量,變量之間的相互賦值不是原子操作)才是原子操作。
舉例:請(qǐng)分析以下哪些操作是原子性操作:
x = 10; //語句1 y = x; //語句2 x++; //語句3 x = x + 1; //語句4其實(shí)只有語句1是原子性操作,其他三個(gè)語句都不是原子性操作。
語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個(gè)語句的會(huì)直接將數(shù)值10寫入到工作內(nèi)存中。
語句2實(shí)際上包含2個(gè)操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存,雖然讀取x的值以及將x的值寫入工作內(nèi)存 這2個(gè)操作都是原子性操作,但是合起來就不是原子性操作了。
同樣的,x++和 x = x+1包括2個(gè)操作:讀取x的值,進(jìn)行加1操作,寫入新的值。
如何保證原子性?
synchronized、Lock、cas原子類工具由于synchronized和Lock能夠保證任一時(shí)刻只有一個(gè)線程執(zhí)行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。其次cas原子類工具。
共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這個(gè)幾個(gè)線程的共享變量
可見性:一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)的被其它線程看到。也就是一個(gè)線程對(duì)共享變量修改的結(jié)果,另一個(gè)線程馬上就能看到修改的值。
如何保證可見性?
volatile、synchronized、Lock要想實(shí)現(xiàn)變量的一定可見,可以使用volatile。另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見性。(其實(shí)還有final,但是它初始化后,值不可更改,所以一般不用它實(shí)現(xiàn)可見性)。
指令重排:CPU在執(zhí)行代碼時(shí),其實(shí)并不一定會(huì)嚴(yán)格按照我們編寫的順序去執(zhí)行,而是可能會(huì)考慮一些效率方面的原因,對(duì)那些先后順序無關(guān)緊要的代碼進(jìn)行重新排序,這個(gè)操作就被稱為指令重排。指令重排在單線程情況下沒有什么影響,但是在多線程就不一定了。
有序性:程序執(zhí)行的順序按照代碼先后的順序執(zhí)行。
如何保證有序性?
volatile、synchronized、Lockvolatile:
volatile原理:Java語言提供了一種稍弱的同步機(jī)制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見的地方,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新寫入的值。
在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
當(dāng)對(duì)非 volatile 變量進(jìn)行讀寫的時(shí)候,每個(gè)線程先從內(nèi)存拷貝變量到CPU緩存中。如果計(jì)算機(jī)有多個(gè)CPU,每個(gè)線程可能在不同的CPU上被處理,這意味著每個(gè)線程可以拷貝到不同的CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步。
當(dāng)一個(gè)變量定義為 volatile 之后,將具備兩種特性:
1.保證此變量對(duì)所有的線程的可見性。當(dāng)一個(gè)線程修改了這個(gè)變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹?#xff0c;什么時(shí)候被寫入主內(nèi)存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)主內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
2.禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個(gè)“l(fā)oad addl $0x0, (%esp)”操作,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指令重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個(gè)CPU訪問內(nèi)存時(shí),并不需要內(nèi)存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理)。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
volatile為什么不能保證原子性?
簡(jiǎn)單的說,修改volatile變量分為四步:
這樣就很容易看出來,前三步都是不安全的,取值和寫回之間,不能保證沒有其他線程修改。原子性需要鎖來保證。(或者可以理解為線程安全需要鎖來保證)。這也就是為什么,volatile只用來保證變量可見性和有序性,但不保證原子性。
1.9.synchronized同步原理
數(shù)據(jù)同步需要依賴鎖,那鎖的同步又依賴誰?synchronized給出的答案是在軟件層面依賴JVM,而j.u.c.Lock給出的答案是在硬件層面依賴特殊的CPU指令。
package com.paddx.test.concurrent; public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("Method 1 start");}} }反編譯結(jié)果:
2 JVM(java虛擬機(jī))
2.1. 運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Jvm內(nèi)存區(qū)域(運(yùn)行時(shí)數(shù)據(jù)區(qū))劃分:
程序計(jì)數(shù)器:當(dāng)前線程的字節(jié)碼執(zhí)行位置的指示器。內(nèi)存空間小,線程私有。如果線程正在執(zhí)行一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器的值則為 (Undefined)。此內(nèi)存區(qū)域是唯一 一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
Java虛擬機(jī)棧:線程私有,生命周期和線程一致。描述的Java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),存儲(chǔ)著局部變量、操作數(shù)棧、動(dòng)態(tài)鏈接和方法出口等。每一個(gè)方法從調(diào)用直至執(zhí)行結(jié)束,就對(duì)應(yīng)著一個(gè)棧幀從虛擬機(jī)棧中入棧到出棧的過程。
局部變量表:存放了編譯期可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference 類型)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)
StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。
OutOfMemoryError:如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,而擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存。
本地方法棧:區(qū)別于 Java 虛擬機(jī)棧的是,Java 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。也會(huì)有StackOverflowError和 OutOfMemoryError 異常。
Java堆:所有對(duì)象實(shí)例分配的區(qū)域。對(duì)于絕大多數(shù)應(yīng)用來說,這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對(duì)象實(shí)例和數(shù)組。內(nèi)部會(huì)劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)??梢晕挥谖锢砩喜贿B續(xù)的空間,但是邏輯上要連續(xù)。
OutOfMemoryError:如果堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),拋出該異常。
方法區(qū):所有已經(jīng)被虛擬機(jī)加載的類的信息、常量、靜態(tài)變量和即時(shí)編輯器編譯后的代碼數(shù)據(jù)
詳細(xì)說明:
程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register) 是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
由于 Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。
因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。
如果線程正在執(zhí)行的是一個(gè) Native 方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。
此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
Java 虛擬機(jī)棧
Java 虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame) 用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等消息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。
局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?#xff0c;也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)。
其中 64 位長(zhǎng)度的 long 和 double 類型的數(shù)據(jù)會(huì)占用兩個(gè)局部變量空間(Slot),其余的數(shù)據(jù)類型只占用一個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
在 Java 虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀態(tài):
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的的深度,將拋出 StackOverflowError 異常。
如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出 OutOfMemoryError 異常。
本地方法棧
本地方法棧(Native Method Stack) 與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。
在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(例如:Sun HotSpot虛擬機(jī))直接就把虛擬機(jī)棧和本地方法棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出 StackOverflowError 和 OutOfMemoryError 異常。
Java 堆
對(duì)于大多數(shù)應(yīng)用來說,Java 堆(Java Heap) 是 Java 虛擬機(jī)所管理的的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。
Java堆是垃圾收集器管理的主要區(qū)域,從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本采用分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)的有 Eden 空間、From Survivor 空間、To Survivor 空間等。
從內(nèi)存分配的角度來看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內(nèi)容無關(guān),無論哪個(gè)區(qū)域,存儲(chǔ)的仍然是對(duì)象實(shí)例,進(jìn)一步劃分的目的是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。
方法區(qū)
方法區(qū)(Method Area)與 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
運(yùn)行時(shí)常量池(Runtime Constant Pool) 是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯器生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)就會(huì)拋出 OutOfMemoryError 異常。
擴(kuò)展:String s1 = "abc"和String s2 = new String(“abc”)的區(qū)別,生成對(duì)象的情況
指向方法區(qū):"abc"是常量,所以它會(huì)在方法區(qū)中分配內(nèi)存,如果方法區(qū)已經(jīng)給"abc"分配過內(nèi)存,則s1會(huì)直接指向這塊內(nèi)存區(qū)域。
指向Java堆:new String(“abc”)是重新生成了一個(gè)Java實(shí)例,它會(huì)在Java堆中分配一塊內(nèi)存。
2.1. GC機(jī)制(重要)
GC 是 garbage collection 的縮寫, 垃圾回收的意思. 也可以是 Garbage Collector, 也就是垃圾回收器.
Java的內(nèi)存分配與回收全部由JVM垃圾回收進(jìn)程自動(dòng)完成。
面試題:“你能不能談?wù)?#xff0c;java GC”
1、哪些對(duì)象可以被回收。 2、何時(shí)回收這些對(duì)象。 3、采用什么樣的方式回收。問題1:哪些對(duì)象可以被回收?
對(duì)象存活判斷(如何判斷對(duì)象可回收/垃圾搜集)
判斷一個(gè)對(duì)象可以回收通常采用的算法是引用計(jì)數(shù)算法和可達(dá)性分析算法。由于互相引用導(dǎo)致的計(jì)數(shù)不好判斷,Java采用的可達(dá)性算法。
引用計(jì)數(shù)算法
每個(gè)對(duì)象有一個(gè)引用計(jì)數(shù)屬性,新增一個(gè)引用時(shí)計(jì)數(shù)加1,引用釋放時(shí)計(jì)數(shù)減1,計(jì)數(shù)為0時(shí)可以回收。此方法簡(jiǎn)單,效率很高,但是主流的JVM并沒有選用這種算法來判定可回收對(duì)象,因?yàn)樗幸粋€(gè)致命的缺陷,那就是它無法解決對(duì)象之間相互循環(huán)引用的的問題,對(duì)于循環(huán)引用的對(duì)象它無法進(jìn)行回收。例:
程序啟動(dòng)后,objectA和objectB兩個(gè)對(duì)象被創(chuàng)建并在堆中分配內(nèi)存,這兩個(gè)對(duì)象都相互持有對(duì)方的引用,除此之外,這兩個(gè)對(duì)象再無任何其他引用,實(shí)際上這兩個(gè)對(duì)象已經(jīng)不可能再被訪問(引用被置空,無法訪問),但是它們因?yàn)橄嗷ヒ弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)器都不為0,于是引用計(jì)數(shù)算法無法通知GC收集器回收它們。
實(shí)際上,當(dāng)?shù)?步執(zhí)行時(shí),兩個(gè)對(duì)象的引用計(jì)數(shù)器值都為1;當(dāng)?shù)?步執(zhí)行時(shí),兩個(gè)對(duì)象的引用計(jì)數(shù)器都為2;當(dāng)?shù)?步執(zhí)行時(shí),二者都清為空值,引用計(jì)數(shù)器值都變?yōu)?。根據(jù)引用計(jì)數(shù)算法的思想,值不為0的對(duì)象被認(rèn)為是存活的,不會(huì)被回收;而事實(shí)上這兩個(gè)對(duì)象已經(jīng)不可能再被訪問了,應(yīng)該被回收。
可達(dá)性分析算法(根搜索算法)
在主流的JVM實(shí)現(xiàn)中,都是通過可達(dá)性分析算法來判定對(duì)象是否存活的??蛇_(dá)性分析算法的基本思想是:通過一系列被稱為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索走過的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots對(duì)象沒有任何引用鏈相連,就認(rèn)為GC Roots到這個(gè)對(duì)象是不可達(dá)的,判定此對(duì)象為不可用對(duì)象,可以被回收。
在上圖中,objectA、objectB、objectC是可達(dá)的,不會(huì)被回收;objectD、objectE雖然有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的,所以它們將會(huì)被判定為是可回收的對(duì)象。
在Java中,可作為GC Roots的對(duì)象包括下面幾種:
虛擬機(jī)棧中引用的對(duì)象
虛擬機(jī)棧中的引用的對(duì)象可以作為GC Root。我們程序在虛擬機(jī)的棧中執(zhí)行,每次函數(shù)調(diào)用調(diào)用都是一次入棧。在棧中包括局部變量表和操作數(shù)棧,局部變量表中的變量可能為引用類型(reference),他們引用的對(duì)象即可作為GC Root。不過隨著函數(shù)調(diào)用結(jié)束出棧,這些引用便會(huì)消失。
方法區(qū)中類靜態(tài)屬性引用的對(duì)象
簡(jiǎn)單的說就是我們?cè)陬愔惺褂玫膕tatic聲明的引用類型字段,例如:
方法區(qū)中常量引用的對(duì)象
簡(jiǎn)單的說就是我們?cè)陬愔惺褂胒inal聲明的引用類型字段,例如:
本地方法棧中引用的對(duì)象
就是程序中native本地方法引用的對(duì)象。
問題3:采用什么樣的方式回收
GC常用算法
可達(dá)性分析算法只是知道了哪些對(duì)象可以回收,不過垃圾收集顯然還需要解決后兩個(gè)問題,什么時(shí)候回收以及如何回收,在根搜索算法的基礎(chǔ)上,現(xiàn)代虛擬機(jī)的實(shí)現(xiàn)當(dāng)中,垃圾搜集的算法主要有三種,分別是標(biāo)記-清除算法、復(fù)制算法、標(biāo)記-整理算法,這三種算法都擴(kuò)充了根搜索算法,不過它們理解起來還是非常好理解的。
標(biāo)記 -清除算法
就是當(dāng)程序運(yùn)行期間,若可以使用的內(nèi)存被耗盡的時(shí)候,GC線程就會(huì)被觸發(fā)并將程序暫停,隨后將依舊存活的對(duì)象標(biāo)記一遍,最終再將堆中所有沒被標(biāo)記的對(duì)象全部清除掉,接下來便讓程序恢復(fù)運(yùn)行。之所以說它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的。
它的主要缺點(diǎn)有兩個(gè):一個(gè)是效率問題,標(biāo)記和清除過程的效率都不高(遞歸與全堆對(duì)象遍歷);另外一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,內(nèi)存的布局自然會(huì)亂七八糟。空間碎片太多可能會(huì)導(dǎo)致,當(dāng)程序在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
復(fù)制算法
“復(fù)制”(Copying)的收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,持續(xù)復(fù)制長(zhǎng)生存期的對(duì)象則導(dǎo)致效率降低。
標(biāo)記-整理算法
復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據(jù)老年代的特點(diǎn),有人提出了另外一種“標(biāo)記-整理”(Mark-Compact)算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。標(biāo)記/整理算法唯一的缺點(diǎn)就是效率也不高,不僅要標(biāo)記所有存活對(duì)象,還要整理所有存活對(duì)象的引用地址。從效率上來說,標(biāo)記/整理算法要低于復(fù)制算法。
分代搜集算法(重要)
GC 分代的基本假設(shè):絕大部分對(duì)象的生命周期都非常短暫,存活時(shí)間短。
“分代搜集”算法,把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?。在新生代?#xff0c;每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來進(jìn)行回收。
新生代GC(minor GC):指發(fā)生在新生代的垃圾回收動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特點(diǎn),所以minor GC發(fā)生得非常頻繁,一般回收速度也比較塊。老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,它的速度會(huì)比minor GC慢很多。問題2:何時(shí)回收這些對(duì)象
回收的時(shí)機(jī)
JVM在進(jìn)行GC時(shí),并非每次都對(duì)上面三個(gè)內(nèi)存區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC),它們所針對(duì)的區(qū)域如下。普通GC(minor GC):只針對(duì)新生代區(qū)域的GC。全局GC(major GC or Full GC):針對(duì)年老代的GC,偶爾伴隨對(duì)新生代的GC以及對(duì)永久代的GC。由于年老代與永久代相對(duì)來說GC效果不好,而且二者的內(nèi)存使用增長(zhǎng)速度也慢,因此一般情況下,需要經(jīng)過好幾次普通GC,才會(huì)觸發(fā)一次全局GC。
內(nèi)存模型與回收策略
Java 堆(Java Heap)是JVM所管理的內(nèi)存中最大的一塊,堆又是垃圾收集器管理的主要區(qū)域,Java 堆主要分為2個(gè)區(qū)域-新生代與老年代,其中年輕代又分 Eden 區(qū)和 Survivor 區(qū),其中 Survivor 區(qū)又分 From 和 To 2個(gè)區(qū)。
Eden 區(qū)
大多數(shù)情況下,對(duì)象會(huì)在新生代 Eden 區(qū)中進(jìn)行分配,當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)會(huì)發(fā)起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也更快。 通過 Minor GC 之后,Eden 會(huì)被清空,Eden 區(qū)中絕大部分對(duì)象會(huì)被回收,而那些無需回收的存活對(duì)象,將會(huì)進(jìn)到 Survivor 的 From 區(qū)(若 From 區(qū)不夠,則直接進(jìn)入 Old 區(qū))。
Survivor 區(qū)
Survivor 區(qū)相當(dāng)于是 Eden 區(qū)和 Old 區(qū)的一個(gè)緩沖,類似于我們交通燈中的黃燈。Survivor 又分為2個(gè)區(qū),一個(gè)是 From 區(qū),一個(gè)是 To 區(qū)。每次執(zhí)行 Minor GC,會(huì)將 Eden 區(qū)和 From 存活的對(duì)象放到 Survivor 的 To 區(qū)(如果 To 區(qū)不夠,則直接進(jìn)入 Old 區(qū))。Survivor 的存在意義就是減少被送到老年代的對(duì)象,進(jìn)而減少 Major GC 的發(fā)生。Survivor 的預(yù)篩選保證,只有經(jīng)歷16次 Minor GC 還能在新生代中存活的對(duì)象,才會(huì)被送到老年代。
Old 區(qū)
老年代占據(jù)著2/3的堆內(nèi)存空間,只有在 Major GC 的時(shí)候才會(huì)進(jìn)行清理,每次 GC 都會(huì)觸發(fā)“Stop-The-World”。內(nèi)存越大,STW 的時(shí)間也越長(zhǎng),所以內(nèi)存也不僅僅是越大就越好。由于復(fù)制算法在對(duì)象存活率較高的老年代會(huì)進(jìn)行很多次的復(fù)制操作,效率很低,所以老年代這里采用的是標(biāo)記——整理算法。
java垃圾收集器:(共7種,著重了解CMS和G1)
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的 Java 應(yīng)用都集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來較好的體驗(yàn)。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括:
初始標(biāo)記(CMS initial mark)
并發(fā)標(biāo)記(CMS concurrent mark)
重新標(biāo)記(CMS remark)
并發(fā)清除(CMS concurrent sweep)
其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行。老年代收集器(新生代使用ParNew)
G1收集器
與CMS收集器相比G1收集器有以下特點(diǎn):
1、空間整合,G1收集器采用標(biāo)記整理算法,不會(huì)產(chǎn)生內(nèi)存空間碎片。分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)空間而提前觸發(fā)下一次GC。
2、可預(yù)測(cè)停頓,這是G1的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS的共同關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為N毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí) Java(RTSJ)的垃圾收集器的特征了。
使用G1收集器時(shí),Java堆的內(nèi)存布局與其他收集器有很大差別,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續(xù))Region 的集合。
G1的新生代收集跟 ParNew 類似,當(dāng)新生代占用達(dá)到一定比例的時(shí)候,開始出發(fā)收集。和 CMS 類似,G1 收集器收集老年代對(duì)象會(huì)有短暫停頓。
2.3. 類加載過程
類加載的時(shí)機(jī):
- 隱式加載:new創(chuàng)建類的實(shí)例
- 顯式加載:loaderClass,forName等
- 操作類的靜態(tài)變量(使用或賦值)
- 調(diào)用類的靜態(tài)方法
- 使用反射方式創(chuàng)建某個(gè)類或者接口對(duì)象的Class對(duì)象。
- 初始化某個(gè)類的子類
- 直接使用java.exe命令來運(yùn)行某個(gè)主類
類的加載過程:
其中加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的。解析階段可以在初始化之后再開始(運(yùn)行時(shí)綁定或動(dòng)態(tài)綁定或晚期綁定)。
加載:ClassLoader通過一個(gè)類的完全限定名查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個(gè)class對(duì)象。
驗(yàn)證:對(duì)類的驗(yàn)證。目的在于確保class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,不會(huì)危害虛擬機(jī)自身的安全,主要包括四種驗(yàn)證:文件格式的驗(yàn)證,元數(shù)據(jù)的驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證。
準(zhǔn)備:為類變量分配內(nèi)存并設(shè)置初始值。
為類變量(static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值,(如static int i = 5 這里只是將 i 賦值為0,在初始化的階段再把 i 賦值為5),這里不包含final修飾的static ,因?yàn)閒inal在編譯的時(shí)候就已經(jīng)分配了。這里不會(huì)為實(shí)例變量分配初始化,類變量會(huì)分配在方法區(qū)中,實(shí)例變量會(huì)隨著對(duì)象分配到Java堆中。
解析:將常量池的符號(hào)引用轉(zhuǎn)化為直接引用。
初始化:這里是類加載的最后階段,前面過程都是以虛擬機(jī)主導(dǎo),而初始化階段開始執(zhí)行類中定義的Java程序代碼,包括類變量的賦值動(dòng)作和構(gòu)造函數(shù)的賦值。
如果該類具有父類就進(jìn)行對(duì)父類進(jìn)行初始化,執(zhí)行其靜態(tài)初始化器(靜態(tài)代碼塊)和靜態(tài)初始化成員變量。(前面已經(jīng)對(duì)static 初始化了默認(rèn)值,這里我們對(duì)它進(jìn)行賦值,成員變量也將被初始化)
使用:
卸載
只有加載、驗(yàn)證、準(zhǔn)備、初始化和卸載的這個(gè)五個(gè)階段的順序是確定的。
2.4. 雙親委派模型
類加載的機(jī)制,以及為什么要這樣設(shè)計(jì)?
類加載的機(jī)制是雙親委派模型。大部分Java程序需要使用的類加載器包括:
- 啟動(dòng)類加載器:由C++語言實(shí)現(xiàn),負(fù)責(zé)加載Java中的核心類。
- 擴(kuò)展類加載器:負(fù)責(zé)加載Java擴(kuò)展的核心類之外的類。
- 應(yīng)用程序類加載器:負(fù)責(zé)加載用戶類路徑上指定的類庫
雙親委派模型如下:
雙親委派模型要求出了頂層的啟動(dòng)類加載器之外,其他的類加載器都有自己的父加載器,通過組合實(shí)現(xiàn)。
雙親委派模型的工作流程/原理:
簡(jiǎn)單說:
當(dāng)一個(gè)類加載的任務(wù)來臨的時(shí)候,先交給父類加載器完成,父類加載器交給父父類加載器完成,知道傳遞給啟動(dòng)類加載器,如果完成不了的情況下,再依次往下傳遞類加載的任務(wù)。
詳細(xì)解釋:
如果一個(gè)類收到了類加載的請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類加載器去執(zhí)行,如果父類加載器還存在父類加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最后到達(dá)頂層的啟動(dòng)類加載器,如果父類能夠完成類的加載任務(wù),就會(huì)成功返回,倘若父類加載器無法完成任務(wù),子類加載器才會(huì)嘗試自己去加載,這就是雙親委派模式。就是每個(gè)兒子都很懶,遇到類加載的活都給它爸爸干,直到爸爸說我也做不來的時(shí)候,兒子才會(huì)想辦法自己去加載。
雙親委派模型的優(yōu)勢(shì)?這樣設(shè)計(jì)的原因?:
簡(jiǎn)單說:
雙親委派模型能夠保證Java程序的穩(wěn)定運(yùn)行,不同層次的類加載器具有不同優(yōu)先級(jí),所有的對(duì)象的父類Object,無論哪一個(gè)類加載器加載,最后都會(huì)交給啟動(dòng)類加載器,保證安全。
詳細(xì)解釋:
采用雙親委派模式的好處就是Java類隨著它的類加載器一起具備一種帶有優(yōu)先級(jí)的層次關(guān)系,通過這種層級(jí)關(guān)系可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時(shí)候,就沒有必要子類加載器(ClassLoader)再加載一次。其次是考慮到安全因素,Java核心API中定義類型不會(huì)被隨意替換,假設(shè)通過網(wǎng)路傳遞一個(gè)名為java.lang.Integer的類,通過雙親委派的的模式傳遞到啟動(dòng)類加載器,而啟動(dòng)類加載器在核心Java API發(fā)現(xiàn)這個(gè)名字類,發(fā)現(xiàn)該類已經(jīng)被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞過來的java.lang.Integer.而之際返回已經(jīng)加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改??赡苣銜?huì)想,如果我們?cè)赾alsspath路徑下自定義一個(gè)名為java.lang.SingInteger該類并不存在java.lang中,經(jīng)過雙親委托模式,傳遞到啟動(dòng)類加載器中,由于父類加載器路徑下并沒有該類,所以不會(huì)加載,將反向委托給子類加載器,最終會(huì)通過系統(tǒng)類加載器加載該類,但是這樣做是不允許的,因?yàn)閖ava.lang是核心的API包,需要訪問權(quán)限,強(qiáng)制加載將會(huì)報(bào)出如下異常。
為什么叫雙親委派?
parents delegate
parents在英文中是“父母”、“雙親”的意思,但其實(shí)表達(dá)的是“父母這一輩”的人的意思。實(shí)際上這個(gè)模型中,只是表達(dá)“父母這一輩”的class loader而已,并不是說真的有一個(gè)父親的class loader和一個(gè)母親class loader。
簡(jiǎn)單來說,就是翻譯的人,不僅英語不好,而且也不理解jvm的類加載機(jī)制,才會(huì)導(dǎo)致翻譯成這樣
先自我介紹一下,小編13年上師交大畢業(yè),曾經(jīng)在小公司待過,去過華為OPPO等大廠,18年進(jìn)入阿里,直到現(xiàn)在。深知大多數(shù)初中級(jí)java工程師,想要升技能,往往是需要自己摸索成長(zhǎng)或是報(bào)班學(xué)習(xí),但對(duì)于培訓(xùn)機(jī)構(gòu)動(dòng)則近萬元的學(xué)費(fèi),著實(shí)壓力不小。自己不成體系的自學(xué)效率很低又漫長(zhǎng),而且容易碰到天花板技術(shù)停止不前。因此我收集了一份《java開發(fā)全套學(xué)習(xí)資料》送給大家,初衷也很簡(jiǎn)單,就是希望幫助到想自學(xué)又不知道該從何學(xué)起的朋友,同時(shí)減輕大家的負(fù)擔(dān)。添加下方名片,即可獲取全套學(xué)習(xí)資料哦
總結(jié)
以上是生活随笔為你收集整理的Android复习系列②之《Java进阶》的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pixel(css pixel dev
- 下一篇: 从零开始在Windows上构建Andro