高級程序員知識學習(併發編程的相關知識)

併發編程有點和缺點

1充分利用多核CPU的計算能力:通過併發編程的形式可以將多核CPU的計算能力發揮到極致,性能得到提升

2方便進行業務拆分,提升系統併發能力和性能:在特殊的業務場景下,先天的就適合於併發編程。現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分

但是併發編程並不總是能提高程序運行速度的,而且併發編程可能會遇到很多問題,比如**:內存泄漏、上下文切換、線程安全、死鎖**等問題。

併發編程三要素(線程的安全性問題體現在):

原子性:原子,即一個不可再被分割的顆粒。原子性指的是一個或多個操作要麼全部執行成功要麼全部執行失敗。

可見性:一個線程對共享變量的修改,另一個線程能夠立刻看到。(synchronized,volatile)

有序性:程序執行的順序按照代碼的先後順序執行。(處理器可能會對指令進行重排序)

並行和併發有什麼區別?

併發:多個任務在同一個 CPU 核上,按細分的時間片輪流(交替)執行,從邏輯上來看那些任務是同時執行。

並行:單位時間內,多個處理器或多核處理器同時處理多個任務,是真正意義上的“同時進行”。

串行:有n個任務,由一個線程按順序執行。由於任務、方法都在一個線程執行所以不存在線程不安全情況,也就不存在臨界區的問題。

做一個形象的比喻:

併發 = 兩個隊列和一臺咖啡機。

並行 = 兩個隊列和兩臺咖啡機。

線程和進程區別

進程是資源分配的最小的單位,線程是執行的最小的單位。

進程是資源分配的最小單位,線程是程序執行的最小單位進程是系統中正在運行的一個程序,程序一旦運行就是進程。一個進程可以擁有多個線程,每個線程使用其所屬進程的棧空間。線程與進程的一個主要區別是,同一進程內的一個主要區別是,同一進程內的多個線程會共享部分狀態,多個線程可以讀寫同一塊內存(一個進程無法直接訪問另一進程的內存)。同時,每個線程還擁有自己的寄存器和棧,其他線程可以讀寫這些棧內存。

什麼是上下文切換?

概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

什麼是線程死鎖

死鎖是指兩個或兩個以上的進程(線程)在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱爲死鎖進程(線程)

多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

形成死鎖的四個必要條件是什麼

互斥條件:線程(進程)對於所分配到的資源具有排它性,即一個資源只能被一個線程(進程)佔用,直到被該線程(進程)釋放

請求與保持條件:一個線程(進程)因請求被佔用資源而發生阻塞時,對已獲得的資源保持不放。

不剝奪條件:線程(進程)已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢後才釋放資源。

循環等待條件:當發生死鎖時,所等待的線程(進程)必定會形成一個環路(類似於死循環),造成永久阻塞

如何避免線程死鎖

破壞互斥條件

這個條件我們沒有辦法破壞,因爲我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

破壞請求與保持條件

一次性申請所有的資源。

破壞不剝奪條件

佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。

破壞循環等待條件

靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

創建線程有哪幾種方式?

    繼承 Thread 類;

    實現 Runnable 接口;

    實現 Callable 接口;

    使用 Executors 工具類創建線程池

說一下 runnable 和 callable 有什麼區別?

相同點

都是接口

都可以編寫多線程程序

都採用Thread.start()啓動線程

主要區別

    Runnable 接口 run 方法無返回值;Callable 接口 call 方法有返回值,是個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果

    Runnable 接口 run 方法只能拋出運行時異常,且無法捕獲處理;Callable 接口 call 方法允許拋出異常,可以獲取異常信息

注:Callalbe接口支持返回執行結果,需要調用FutureTask.get()得到,此方法會阻塞主進程的繼續往下執行,如果不調用不會阻塞。

如何停止一個正在運行的線程?

在java中有以下3種方法可以終止正在運行的線程:

    使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。

    使用stop方法強行終止,但是不推薦這個方法,因爲stop和suspend及resume一樣都是過期作廢的方法。

    使用interrupt方法中斷線程。

如果你提交任務時,線程池隊列已滿,這時會發生什麼

這裏區分一下:

(1)如果使用的是無界隊列 LinkedBlockingQueue,也就是無界隊列的話,沒關係,繼續添加任務到阻塞隊列中等待執行,因爲 LinkedBlockingQueue 可以近乎認爲是一個無窮大的隊列,可以無限存放任務

(2)如果使用的是有界隊列比如 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增加線程數量,如果增加了線程數量還是處理不過來,ArrayBlockingQueue 繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy。

在 Java 程序中怎麼保證多線程的運行安全?

    方法一:使用安全類,比如 java.util.concurrent 下的類,使用原子類AtomicInteger

    方法二:使用自動鎖 synchronized。

    方法三:使用手動鎖 Lock。

JMM模型

synchronize的關鍵字

他屬於獨佔式的悲觀鎖,同時屬於可重入鎖。

代碼塊同步是使用monitorenter和monitorexit指令實現的。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

同步代碼塊是通過 monitorenter 和 monitorexit 指令獲取線程的執行權。

同步方法通過加 ACC_SYNCHRONIZED 標識實現線程的執行權的控制。

synchronized可重入的原理

重入鎖是指一個線程獲取到該鎖之後,該線程可以繼續獲得該鎖。底層原理維護一個計數器,當線程獲取該鎖時,計數器加一,再次獲得該鎖時繼續加一,釋放鎖時,計數器減一,當計數器值爲0時,表明該鎖未被任何線程所持有,其它線程可以競爭獲取鎖。

對象有什麼轉態:無鎖 偏向鎖 輕量鎖重量鎖 GC標記

Synchronize是如何實現的同步的?

Synchronize 是鎖住的對象的,不能去鎖住代碼塊的。另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

多線程中 synchronized 鎖升級的原理是什麼?

synchronized 鎖升級原理:在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖爲輕量級鎖,通過自旋循環一定次數來獲取鎖,執行一定次數之後,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖,此過程就構成了 synchronized 鎖的升級。

鎖的升級的目的:鎖升級是爲了減低了鎖帶來的性能消耗。在 Java 6 之後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。

樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?

悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。

樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

樂觀鎖的實現方式:

1、使用版本標識來確定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。

2、java 中的 Compare and Swap 即 CAS ,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。 CAS 操作中包含三個操作數 —— 需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置 V 的值與預期原值 A 相匹配,那麼處理器會自動將該位置值更新爲新值 B。否則處理器不做任何操作。

ReentrantLock的實現

實現的原理是通過一個標誌位置來實現鎖的實現。來實現是否可以獲得鎖的機制。

對象包括三個部分:對象的佈局Synchronize在JDK1.6之前的時候採用的是調用的是private native void start();這個函數中調用了native的方法的時候會調用的操作系統的線程。CPU造成(內核態—用戶態)在JDk1.7中在JVM級別就解決了鎖問題。Lock的只要是通過利用了自旋的問題 park-unpark CAS的技術在單個線程和交替執行的其實是和隊列無關的在jdk的級別解決同步的問題。

synchronized 和 ReentrantLock 區別是什麼?

synchronized 競爭鎖時會一直等待;ReentrantLock 可以嘗試獲取鎖,並得到獲取結果

synchronized 獲取鎖無法設置超時;ReentrantLock 可以設置獲取鎖的超時時間

synchronized 無法實現公平鎖;ReentrantLock 可以滿足公平鎖,即先等待先獲取到鎖

synchronized 控制等待和喚醒需要結合加鎖對象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和喚醒需要結合 Condition 的 await() 和 signal()、signalAll() 方法

synchronized 是 JVM 層面實現的;ReentrantLock 是 JDK 代碼層面實現

synchronized 在加鎖代碼塊執行完或者出現異常,自動釋放鎖;ReentrantLock 不會自動釋放鎖,需要在 finally{} 代碼塊顯示釋放

自旋鎖yield的原理

Yield的方式就是讓出cpu的資源,但是這樣的方式只適合於在只有兩個進程的時候出現的情況下。如果是出現的是多線程的時候可能不會讓其他進程獲得cpu的資源。因爲系統是隨機分配的資源。有可能還是分配給原來的進程。線程自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那線程也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態

sleep鎖原理:

可能存在的是sleep的時候很久但是還是沒有釋放鎖資源。不好確定sleep的時間。

自旋

什麼是自旋

很多 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間非常快,此時等待的線程都加鎖可能是一種不太值得的操作,因爲線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得非常快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界做忙循環,這就是自旋。如果做了多次循環發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。

多線程中 synchronized 鎖升級的原理是什麼?

synchronized 鎖升級原理:在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖爲輕量級鎖,通過自旋循環一定次數來獲取鎖,執行一定次數之後,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖,此過程就構成了 synchronized 鎖的升級。

鎖的升級的目的:鎖升級是爲了減低了鎖帶來的性能消耗。在 Java 6 之後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。

線程 B 怎麼知道線程 A 修改了變量

(1)volatile 修飾變量

(2)synchronized 修飾修改變量的方法

(3)wait/notify

(4)while 輪詢

Park_自旋

就是讓線程掛起的一種狀態。LockSupport.park()睡眠線程。LockSupport.unpark()立即喚醒線程。

volatile 關鍵字的作用

Java 語言提供了一種稍弱的同步機制, volatile 變量,用來確保將變量的更新操作通知到其他線程。volatile 變量具備兩種特性, volatile 變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取 volatile 類型的變量時總會返回最新寫入的值。

併發編程的設計模式

Future的模式

創建線程的2種方式,一種是直接繼承Thread,另外一種就是實現Runnable接口。 這2種方式都有一個缺陷就是:在執行完任務之後無法獲取執行結果。如果需要獲取執行結果,就必須通過共享變量或者使用線程通信的方式來達到效果,這樣使用起來就比較麻煩。就提供了Callable和Future,通過它們可以在任務執行完畢之後得到任務執行結果

Future模式的核心就是,使原本需要等待的時間段可以用於處理其他業務邏輯。該模式主要用於並行處理多個互不影響的請求,最後將結果彙總的業務。可以對多個不同的請求啓動多個不同的線程,然後在線程中獨立去獲取想要的結果,等到真正需要使用該數據的時候纔會獲取到真正的結果。和AJAX的作用是相同的就是一個異步的調用的。

Master-Worker模式

常用的並行計算模式。他的核心思想是系統由兩類進程協作工作:Master進程和Worker進程。Master負責接收和分配任務,Worker負責處理子任務。當各個Worker子進程處理完成後,會將結果返回給Master,Master做歸納和總結。其好處是能將一個大任務分解成若干個小任務,並行執行,從而提高系統的吞吐量。

生產者-消費者模式

生產者和消費者也是一個非常經典的多線程模式,我們在實際的開發中應用非常廣泛的思想理念。在生成-消費模式中:通常由兩類線程,即若干個生產者的線程和若干個消費者的線程。生產者線程負責提交用戶請求,消費者線程則負責具體處理生產者提交的任務,在生產者和消費者之間通過共享內存緩存進行通信。

ThreadLocal 作用(線程本地存儲

它是一種爲共享變量在每一個線程中創建一個副本,每一個線程都是可以訪問自己的副本的變量。通過綁定線程來實現對線程副本的操作。而不影響其他線程。

目的就是爲了解決在多線程下訪問一個變量的下數據的一致性。

ThreadLocal造成內存泄漏的原因?

ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,而 value 是強引用。所以,如果 ThreadLocal沒有被外部強引用的情況下,在垃圾回收的時候,key會被清理掉,而value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如我們不做任何措施的話,value 永遠無法被GC 回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 爲 null 的記錄。使用完 ThreadLocal方法後 最好手動調用remove()方法

ThreadLocal內存泄漏解決方案?

每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

AQS鎖

AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMS8yNS8xNmVhMDQ3Njc4NGNkMzJi?x-oss-process=image/format,png

公平鎖和非公平鎖

ReentrackLock在默認的實現的是非公平鎖,但是也是可以實現公平鎖。Reetracklock當被人持有的是就是不自由的。

JVM 按隨機、就近原則分配鎖的機制則稱爲不公平鎖, ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。 非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。

公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。

重量鎖和輕量鎖

重量鎖就是設計到操作內核或者是os系統函數的鎖,輕量鎖是在JVM層級所涉及到的鎖。

在JDK 1.6之前在synchronize是性能低下的鎖。因爲應用到了os系統。

“輕量級”:是相對於使用操作系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程之前, 先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹爲重量級鎖。

JVM 按隨機、就近原則分配鎖的機制則稱爲不公平鎖, ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。 非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。

公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。

重量鎖和輕量鎖

重量鎖就是設計到操作內核或者是os系統函數的鎖,輕量鎖是在JVM層級所涉及到的鎖。

在JDK 1.6之前在synchronize是性能低下的鎖。因爲應用到了os系統。

“輕量級”:是相對於使用操作系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程之前, 先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹爲重量級鎖。

重入鎖

線程池的原理

什麼是線程池?

線程池就是創建若干個可執行的線程放入一個池(容器)中,有任務需要處理時,會提交到線程池中的任務隊列,處理完之後線程並不會被銷燬,而是仍然在線程池中等待下一個任務。

爲什麼要使用線程池?

因爲 Java 中創建一個線程,需要調用操作系統內核的 API,操作系統要爲線程分配一系列的資源,成本很高,所以線程是一個重量級的對象,應該避免頻繁創建和銷燬。使用線程池就能很好地避免頻繁創建和銷燬。

JDK 1.8 中,線程池的停止一般使用 shutdown()、shutdownNow()、shutdown() + awaitTermination(long timeout, TimeUnit unit) 方法。

線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等候,等其它線程執行完畢,再從隊列中取出任務來執行。他的主要特點:線程複用:控制最大併發數;管理線程。

1. 線程池管理器:用於創建並管理線程池

2. 工作線程:線程池中的線程

3. 任務接口:每個任務必須實現的接口,用於工作線程調度其運行

4. 任務隊列:用於存放待處理的任務,提供一種緩衝機制

工作流程:

創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期異步任務的程序而言,這些線程池通常可提高程序性能。 調用 execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。 因此,長時間保持空閒的線程池不會使用任何資源。

創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。在任意點,在大多數 nThreads線程會處於處理任務的活動狀態。如果在所有線程處於活動狀態時提交附加任務,則在有可用線程之前,附加任務將在隊列中等待。如果在關閉前的執行期間由於失敗而導致任何線程終止,那麼一個新線程將代替它執行後續的任務(如果需要)。在某個線程被顯式地關閉之前,池中的線程將一直存在。Executors.newSingleThreadExecutor()返回一個線程池(這個線程池只有一個線程) ,這個線程池可以在線程死後(或發生異常時)重新啓動一個線程來替代原來的線程繼續執行下去!

Executors 類是從 JDK 1.5 開始就新增的線程池創建的靜態工廠類,它就是創建線程池的,但是很多的大廠已經不建議使用該類去創建線程池。原因在於,該類創建的很多線程池的內部使用了無界任務隊列,在併發量很大的情況下會導致 JVM 拋出 OutOfMemoryError,直接讓 JVM 崩潰,影響嚴重。

樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

java 中的樂觀鎖基本都是通過 CAS 操作實現的, CAS 是一種更新的原子操作, 比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。

悲觀鎖

悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。java中的悲觀鎖就是 Synchronized,AQS框架下的鎖則是先嚐試 cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如 RetreenLock。

可重入鎖(遞歸鎖)

自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降爲0時才能釋放鎖。

ReadWriteLock 讀寫鎖

爲了提高性能, Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率。 讀寫鎖分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm 自己控制的,你只要上好相應的鎖即可

讀鎖:如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖

寫鎖:如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!

Java中讀寫鎖有個接口 java.util.concurrent.locks.ReadWriteLoc,也 有 具 體 的 實 現ReentrantReadWriteLock

共享鎖和獨佔鎖

獨佔鎖獨佔鎖模式下,每次只能有一個線程能持有鎖, ReentrantLock 就是以獨佔方式實現的互斥鎖。獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,如果某個只讀線程獲取鎖,則其他讀線程都只能等待,這種情況下就限制了不必要的併發性,因爲讀操作並不會影響數據的一致性。

共享鎖:共享鎖則允許多個線程同時獲取鎖,併發訪問 共享資源,如: ReadWriteLock。 共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源。

1. AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 隊列中等待線程的鎖獲取模式。

2. java 的併發包中提供了 ReadWriteLock,讀-寫鎖。它允許一個資源可以被多個讀操作訪問,或者被一個 寫操作訪問,但兩者不能同時進行.

線程生命週期

線程上下文切換

CAS 操作

CAS(Compare And Swap/Set)比較並交換,CAS 算法的過程是這樣:它包含3個參數CAS(V,E,N)。 V表示要更新的變量(內存值),E 表示預期值(舊的),N 表示新值。當且僅當V 值等於 E 值時,纔會將 V 的值設爲 N,如果 V 值和 E 值不同,則說明已經有其他線程做了更新,則當

前線程什麼都不做。最後, CAS 返回當前 V 的真實值。

CAS 操作是抱着樂觀的態度進行的(樂觀鎖),它總是認爲自己可以成功完成操作。當多個線程同時使用 CAS 操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的線程不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作。基於這樣的原理,CAS 操作即使沒有鎖,也可以發現其他線程對當前線程的干擾,並進行恰當的處理。

Atomic關鍵字

Atomic包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操作時,具有排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程可以向自旋鎖一樣,繼續嘗試,一直等到執行成功。

AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大爲提升。

CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。

ABA 問題

CAS 會導致“ABA 問題”。 一個線程 a 將數值改成了 b,接着又改成了 a,此時 CAS 認爲是沒有變化,其實是已經變化過了,而這個問題的解決方案可以使用版本號標識,每操作一次version 加 1。在 java5 中,已經提供了 AtomicStampedReference 來解決問題。CAS 機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證 3 個變量共同進行原子性的更新,就不得不使用 synchronized 了。之前說過了 CAS 裏面是一個循環判斷的過程,如果線程一直沒有獲取到狀態,cpu資源會一直被佔用。

鎖升級的原理

鎖的級別從低到高:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖

鎖分級別原因:

沒有優化以前,synchronized 是重量級鎖(悲觀鎖),使用 wait 和 notify、notifyAll 來切換線程狀態非常消耗系統資源;線程的掛起和喚醒間隔很短暫,這樣很浪費資源,影響性能。所以 JVM 對 synchronized 關鍵字進行了優化,把鎖分爲 無鎖、偏向鎖、輕量級鎖、重量級鎖 狀態。

無鎖:沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功,其他修改失敗的線程會不斷重試直到修改成功。

偏向鎖:對象的代碼一直被同一線程執行,不存在多個線程競爭,該線程在後續的執行中自動獲取鎖,降低獲取鎖帶來的性能開銷。偏向鎖,指的就是偏向第一個加鎖線程,該線程是不會主動釋放偏向鎖的,只有當其他線程嘗試競爭偏向鎖纔會被釋放。

偏向鎖的撤銷,需要在某個時間點上沒有字節碼正在執行時,先暫停擁有偏向鎖的線程,然後判斷鎖對象是否處於被鎖定狀態。如果線程不處於活動狀態,則將對象頭設置成無鎖狀態,並撤銷偏向鎖;

如果線程處於活動狀態,升級爲輕量級鎖的狀態。

輕量級鎖:輕量級鎖是指當鎖是偏向鎖的時候,被第二個線程 B 所訪問,此時偏向鎖就會升級爲輕量級鎖,線程 B 會通過自旋的形式嘗試獲取鎖,線程不會阻塞,從而提高性能。

當前只有一個等待線程,則該線程將通過自旋進行等待。但是當自旋超過一定的次數時,輕量級鎖便會升級爲重量級鎖;當一個線程已持有鎖,另一個線程在自旋,而此時又有第三個線程來訪時,輕量級鎖也會升級爲重量級鎖。

重量級鎖:指當有一個線程獲取鎖之後,其餘所有等待獲取該鎖的線程都會處於阻塞狀態。

重量級鎖通過對象內部的監視器(monitor)實現,而其中 monitor 的本質是依賴於底層操作系統的 Mutex Lock 實現,操作系統實現線程之間的切換需要從用戶態切換到內核態,切換成本非常高。

synchronized 鎖升級的過程:

在鎖對象的對象頭裏面有一個 threadid 字段,未訪問時 threadid 爲空

第一次訪問 jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id

再次訪問時會先判斷 threadid 是否與其線程 id 一致。如果一致則可以直接使用此對象;如果不一致,則升級偏向鎖爲輕量級鎖,通過自旋循環一定次數來獲取鎖

執行一定次數之後,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖。

鎖升級的目的是爲了減低鎖帶來的性能消耗,在 Java 6 之後優化 synchronized 爲此方式。

在Java中,鎖共有4種狀態,級別從低到高依次爲:無狀態鎖,偏向鎖,輕量級鎖和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級。

怎麼樣來升級這些鎖呢?通過一個標誌位來判斷 兩個位置00表示的4種鎖

開啓線程的三種方法:

方式1:繼承Thread類,

           (1)創建線程類對象: A類 a = new A類();

(2)調用線程對象的start方法: a.start();//啓動一個線程

方式2:實現Runnable接口

 (1)創建線程類對象:  Thread  t = new Thread(new  A());    

    (2)調用線程對象的start方法:t.start();

方法3:直接在函數體使用

void java_thread()

{

     Thread t = new Thread(new Runnable(){

            public void run(){

            mSoundPoolMap.put(index, mSoundPool.load(filePath, index));

            getThis().LoadMediaComplete();

            }});

              t.start();

}

notify()和notifyAll()有什麼區別?

等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池,等待池中的線程不會去競爭該對象的鎖。

鎖池:只有獲取了對象的鎖,線程才能執行對象的 synchronized 代碼,對象的鎖每次只有一個線程可以獲得,其他線程只能在鎖池中等待。

區別:

notify() 方法隨機喚醒對象的等待池中的一個線程,進入鎖池;notifyAll() 喚醒對象的等待池中的所有線程,進入鎖池。

如何保證多個線程同時啓動?

可以 wait()、notify() 實現;也可以使用發令槍 CountDownLatch 實現。

併發編程面試問題

synchronized、volatile、CAS 比較

(1)synchronized 是悲觀鎖,屬於搶佔式,會引起其他線程阻塞。

(2)volatile 提供多線程共享變量可見性和禁止指令重排序優化。

(3)CAS 是基於衝突檢測的樂觀鎖(非阻塞)

Java中垃圾回收有什麼目的?什麼時候進行垃圾回收?

垃圾回收是在內存中存在沒有引用的對象或超過作用域的對象時進行的。

垃圾回收的目的是識別並且丟棄應用不再使用的對象來釋放和重用資源。

synchronized 和 volatile 的區別是什麼?

synchronized 表示只有一個線程可以獲取作用對象的鎖,執行代碼,阻塞其他線程。

volatile 表示變量在 CPU 的寄存器中是不確定的,必須從主存中讀取。保證多線程環境下變量的可見性;禁止指令重排序。

區別

    volatile 是變量修飾符;synchronized 可以修飾類、方法、變量。

    volatile 僅能實現變量的修改可見性,不能保證原子性;而 synchronized 則可以保證變量的修改可見性和原子性。

    volatile 不會造成線程的阻塞;synchronized 可能會造成線程的阻塞。

    volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。

volatile關鍵字是線程同步的輕量級實現,所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用於變量而synchronized關鍵字可以修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些。

 

synchronized 和 ReentrantLock 的區別:

sleep 與 wait 區別:

1. 對於 sleep()方法,我們首先要知道該方法是屬於 Thread 類中的。而 wait()方法,則是屬於Object 類中的。

2. sleep()方法導致了程序暫停執行指定的時間,讓出 cpu 該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。

3. 在調用 sleep()方法的過程中, 線程不會釋放對象鎖

4. 而當調用 wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用 notify()方法後本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態。

CAS線程模型、線程池的的原理wait notify

有鎖lock Future Callable

同步、異步、Synchronize volatile

協同cyclicBarrier Semaphonre

  1. 如何創建線程?如何保證線程安全?
  1. 繼承Thread類創建線程:定義Thread類的子類,並重寫該類的run()方法,該方法的方法體就是線程需要完成的任務,run()方法也稱爲線程執行體。創建Thread子類的實例,也就是創建了線程對象啓動線程,即調用線程的start()方法
  2. 實現Runnable接口創建線程:定義Runnable接口的實現類,一樣要重寫run()方法,這個run()方法和Thread中的run()方法一樣是線程的執行體創建Runnable實現類的實例,並用這個實例作爲Thread的target來創建Thread對象,這個Thread對象纔是真正的線程對象第三部依然是通過調用線程對象的start()方法來啓動線程
  3. 使用Callable和Future創建線程:Callable接口提供了一個call()方法作爲線程執行體,call()方法比run()方法功能要強大。創建Callable接口的實現類,並實現call()方法,然後創建該實現類的實例使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了Callable對象的call()方法的返回值

確保線程安全的方法有這幾個:

  1. 競爭與原子操作

多個線程同時訪問和修改一個數據,可能造成很嚴重的後果。出現嚴重後果的原因是很多操作被操作系統編譯爲彙編代碼之後不止一條指令,因此在執行的時候可能執行了一半就被調度系統打斷了而去執行別的代碼了。一般將單指令的操作稱爲原子的(Atomic),因爲不管怎樣,單條指令的執行是不會被打斷的。因此,爲了避免出現多線程操作數據的出現異常,Linux系統提供了一些常用操作的原子指令,確保了線程的安全。但是,它們只適用於比較簡單的場合,在複雜的情況下就要選用其他的方法了。

  1. 同步與鎖

爲了避免多個線程同時讀寫一個數據而產生不可預料的後果,開發人員要將各個線程對同一個數據的訪問同步,也就是說,在一個線程訪問數據未結束的時候,其他線程不得對同一個數據進行訪問。同步的最常用的方法是使用鎖(Lock),它是一種非強制機制,每個線程在訪問數據或資源之前首先試圖獲取鎖,並在訪問結束之後釋放。鎖在鎖已經被佔用的時候試圖獲取鎖時,線程會等待,直到鎖重新可用。二元信號量是最簡單的一種鎖,它只有兩種狀態:佔用與非佔用,它適合只能被唯一一個線程獨佔訪問的資源。對於允許多個線程併發訪問的資源,要使用多元信號量(簡稱信號量)。

  1. 可重入

一個函數被重入,表示這個函數沒有執行完成,但由於外部因素或內部因素,又一次進入該函數執行。一個函數稱爲可重入的,表明該函數被重入後不會產生任何不良後果。可重入是併發安全的強力保障,一個可重入的函數可以在多線程環境下放心使用。

  1. 過度優化

在很多情況下,即使我們合理地使用了鎖,也不一定能夠保證線程安全,因此,我們可能對代碼進行過度的優化以確保線程安全。我們可以使用volatile關鍵字試圖阻止過度優化,它可以做兩件事:第一,阻止編譯器爲了提高速度將一個變量緩存到寄存器而不寫回;第二,阻止編譯器調整操作volatile變量的指令順序。在另一種情況下,CPU的亂序執行讓多線程安全保障的努力變得很困難,通常的解決辦法是調用CPU提供的一條常被稱作barrier的指令,它會阻止CPU將該指令之前的指令交換到barrier之後,反之亦然。

  1. 如何實現一個線程安全的數據結構

所謂 線程安全 就是:一段操縱共享數據的代碼能夠保證在同一時間內被多個線程執行而仍然保持其正確性的,就被稱爲是線程安全的。

HashTable使用synchronized來修飾方法函數來保證線程安全,但是在多線程運行環境下效率表現非常低下。因爲當一個線程訪問HashTable的同步方法時,其他線程也訪問同步方法就會粗線阻塞狀態。比如當一個線程在添加數據時候,另外一個線程即使執行獲取其他數據的操作也必須被阻塞,大大降低了程序的運行效率。

ConcurrentHashMap是HashMap的線程安全版。ConcurrentHashMap允許多個修改操作併發運行,其原因在於使用了鎖分段技術:首先講Map存放的數據分成一段一段的存儲方式,然後給每一段數據分配一把鎖,當一個線程佔用鎖訪問其中一個段的數據時,其他段的數據也能被其他線程訪問。這樣就保證了每一把鎖只是用於鎖住一部分數據,那麼當多線程訪問Map裏的不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效提高併發訪問效率。

CopyOnWriteArrayList實現了List接口,提供的數據更新操作都使用了ReentrantLock的lock()方法來加鎖,unlock()方法來解鎖。

CopyOnWriteArraySet是對CopyOnWriteArrayList使用了裝飾模式後的具體實現。所以CopyOnWriteArrayList的實現機理適用於CopyOnWriteArraySet

ConcurrentLinkedQueue可以被看作是一個線程安全的LinkedList,使用了非阻塞算法實現的一個高效、線程安全的併發隊列。

Vector通過數組保存數據,繼承了Abstract,實現了List;所以,其本質上是一個隊列。但是和ArrayList不同,Vector中的操作是線程安全的,它是利用synchronized同步鎖機制進行實現,其實現方式與HashTable類似。

StringBuffer是通過對方法函數進行synchronized修飾實現其線程安全特性,實現方式與HashTable、Vector類似。

如何避免死鎖

死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

  1. 互斥使用,即當資源被一個線程使用(佔有)時,別的線程不能使用
  2. 不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。
  3. 請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的佔有。
  4. 循環等待,即存在一個等待隊列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了一個等待環路

產生原因:

  1. 競爭資源引起進程死鎖

當系統中供多個進程共享的資源如打印機、公用隊列的等,其數目不足以滿足諸進程的需要時,會引起諸進程對資源的競爭而產生死鎖。

  1. 可剝奪資源和不可剝奪資源

系統中的資源可以分爲兩類,一類是可剝奪資源,是指某進程在獲得這類資源後,該資源可以再被其他進程或系統剝奪。例如,優先權高的進程可以剝奪優先權低的進程的處理機。又如,內存區可由存儲器管理程序,把一個進程從一個存儲區移到另一個存儲區,此即剝奪了該進程原來佔有的存儲區,甚至可將一進程從內存調到外存上,可見,CPU和主存均屬於可剝奪性資源。另一類資源是不可剝奪資源,當系統把這類資源分配給某進程後,再不能強行收回,只能在進程用完後自行釋放,如磁帶機、打印機等。

競爭不可剝奪資源

在系統中所配置的不可剝奪資源,由於它們的數量不能滿足諸進程運行的需要,會使進程在運行過程中,因爭奪這些資源而陷於僵局。例如,系統中只有一臺打印機R1和一臺磁帶機R2,可供進程P1和P2共享。假定PI已佔用了打印機R1,P2已佔用了磁帶機R2,若P2繼續要求打印機R1,P2將阻塞;P1若又要求磁帶機,P1也將阻塞。於是,在P1和P2之間就形成了僵局,兩個進程都在等待對方釋放自己所需要的資源,但是它們又都因不能繼續獲得自己所需要的資源而不能繼續推進,從而也不能釋放自己所佔有的資源,以致進入死鎖狀態。

競爭臨時資源

上面所說的打印機資源屬於可順序重複使用型資源,稱爲永久資源。還有一種所謂的臨時資源,這是指由一個進程產生,被另一個進程使用,短時間後便無用的資源,故也稱爲消耗性資源,如硬件中斷、信號、消息、緩衝區內的消息等,它也可能引起死鎖。

防止死鎖的方法:

死鎖預防(有序資源分配法、銀行家算法):

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

死鎖避免

系統對進程發出的每一個系統能夠滿足的資源申請進行動態檢查,並根據檢查結果決定是否分配資源;如果分配後系統可能發生死鎖,則不予分配,否則予以分配。這是一種保證系統不進入死鎖狀態的動態策略。

死鎖檢測和解除

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

Volatile關鍵字的作用?

volatile是Java提供的一種輕量級的同步機制。Java包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量,相比於synchronized(synchronized通常稱爲重量級鎖),volatile更輕量級,因爲它不會引起線程上下文的切換和調度。但是volatile 變量的同步性較差(有時它更簡單並且開銷更低),而且其使用也更容易出錯。

保證可見性,不保證原子性:當寫一個volatile變量時,JMM會把該線程本地內存中的變量強制刷新到主內存中去;這個寫會操作會導致其他線程中的緩存無效。

禁止指令重排:a.重排序操作不會對存在數據依賴關係的操作進行重排序。b.重排序是爲了優化性能,但是不管怎麼重排序,單線程下程序的執行結果不能被改變。

Run()方法和start()方法的區別

系統調用線程類的start()方法來啓動一個線程,此時該線程處於就緒狀態,而非運行狀態,也就意味着這個線程可以被JVM來調度執行。在調度過程中,JVM通過調用線程類的run()方法來完成實際的操作,當run()方法結束後,此線程就會終止。

​如果直接調用線程類的run()方法,這會被當做一個普通的函數調用,程序中仍然只有主線程這一個線程,也就是說,start()方法能夠異步地調用run()方法,但是直接調用run()方法卻是同步的,因此也就無法達到多線程的目的。

只有通過調用線程類的start()方法才能真正達到多線程的目的。當用start()開始一個線程後,線程就進入就緒狀態,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM調度並執行。但是這並不意味着線程就會立即運行。只有當cpu分配時間片時,這個線程獲得時間片時,纔開始執行run()方法。start()是方法,它調用run()方法.而run()方法是你必須重寫的. run()方法中包含的是線程的主體(真正的邏輯)。

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