多線程
- 1、什么是線程和進程
- 2、創建線程有幾種方式
- 3、線程有幾種狀態
- 4、什么是上下文切換
- 5、什么是守護線程,和普通線程有什么區別
- 6、什么是線程池,如何實現的
- 7、Executor和Executors的區別
- 8、線程池處理任務的流程
- 9、線程數設定成多少更合適
- 10、執行execute方法和submit方法的區別
- 11、什么是ThreadLocal,如何實現的
- 12、父子線程之間怎么共享數據
- 13、線程同步的方式有哪些
- 14、synchronized 是怎么實現的
- 15、synchronized 的鎖升級過程是怎樣的
- 16、什么是鎖消除和鎖粗化
- 17、synchronized 和 reentrantLock 區別
- 18、synchronized 和 Lock 有什么區別
- 19、CountDownLatch、CyclicBarrier、Semaphore
- 20、volatile 是如何保證可見性和有序性的不能保證原子性的
- 21、synchronized 和 volatile 有什么區別
- 22、什么是死鎖,如何解決
- 23、介紹一下 Atomic 原子類
- 24、什么是CAS,存在什么問題
- 25、如何理解AQS
- 26、什么是 Java 內存模型
- 27、三個線程分別順序打印 0-100
- 28、JMM(Java Memory Model)
1、什么是線程和進程
- 什么是進程
是包含了某些資源的內存區域,操作系統利用進程把它的工作劃分為一些功能單元。電腦中時會有很多單獨運行的程序,每個程序有一個獨立的進程。例如微信,IDEA,GOOGLE等等。
- 什么是線程
進程中包含的一個或多個執行單元稱為線程,線程只能歸屬一個進程,并且線程只能訪問該進程擁有的資源。當操作系統創建一個進程,該進程會自動申請一個主線程作為首要的執行任務。線程的切換耗時小,把線程稱為輕負荷線程。一個進程由一個或多個線程組成,彼此間完成不同的工作,多個線程同時執行,稱為多線程
- 進程和線程的關系
- 一個進程由一個或多個線程組成。
- 線程的劃分尺度小于進程。
- 多個進程在執行過程中擁有獨立的內存單元,而多個線程共享內存。
- 什么是并發和并行
- 并發 是多個任務在同一時間段內交替執行,目標是提高資源利用率和響應能力。
- 并行 是多個任務在同一時刻同時執行,目標是提高計算性能。
2、創建線程有幾種方式
- 通過繼承Thread類,重寫run方法,線程的任務定義在run()方法中。
class TicketThread extends Thread {private int tickets = 30;@Overridepublic void run() {while (true) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "正在買票" + tickets--);} else {System.out.println(Thread.currentThread().getName() + "票賣完了");break;}}}
}
創建線程對象和啟動線程
// 創建一個線程對象
TicketThread t = new TicketThread();
// 啟動線程
t.start();
- 實現Runnable接口,實現run方法,線程的任務,定義在run()方法中。
class TicketThread implements Runnable {private int tickets = 30;@Overridepublic void run() {while (true) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "正在買票" + tickets--);} else {System.out.println(Thread.currentThread().getName() + "票賣完了");break;}}}
}
創建線程對象和啟動線程
// 創建一個任務對象
Runnable runnable = new TicketThread();
// 創建一個線程對象
Thread t = new Thread(runnable);
// 啟動線程
t.start();
- 實現Callable接口。
class TicketThread implements Callable<List<String>> {private int tickets = 30;List<String> list = new ArrayList<String>();@Overridepublic List<String> call() throws Exception {while (true) {if (tickets > 0) {list.add(Thread.currentThread().getName() + "正在買票" + tickets--);} else {list.add(Thread.currentThread().getName() + "票賣完了");return list;}}}
}
創建線程對象和啟動線程
Callable callable = new TicketThread();
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// 獲取返回值
List<String> list = (List<String>)futureTask.get();
for (String s : list) {System.out.println(s);
}
- 通過線程池創建線程。
public class H {public static void main(String[] args) {// 1.創建一個單線程的線程池,這個線程池只有一個線程在工作,即單線程執行任務,如果這個唯一的線程因為異常結束,那么就會有一個新的線程來替代它因此線程池保證所有的任務是按照任務的提交順序來執行。// ExecutorService service = Executors.newSingleThreadScheduledExecutor();// 2.創建一個固定大小的線程池,每次提交一個任務就創建一個線程直到達到線程池的最大的大小,線程池的大小一旦達到最大就會保持不變,如果某個線程因為執行異常而結束,那么就會補充一個新的線程。// ExecutorService service = Executors.newFixedThreadPool(5);// 3.創建一個可以緩沖的線程池,如果線程大小超過處理任務所需的線程,那么就會回收部分線程,當線程數增加的時候此線程池不會對線程池大小做限制,線程池的大小完全依賴操作系統能夠創建的做大做小。// ExecutorService service = Executors.newCachedThreadPool();// 4.周期性線程池創建,此線程池支持定時以及周期性的執行任務的需求。// 5.手動創建線程池。ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 30L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(5), new RejectedExecutionHandler() {// 回調方法public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {System.out.println("線程數超過了線程池容量,拒絕執行任務-->" + r);}});// 執行10個任務for (int i = 0; i < 10; i++) {threadPool.execute(new Runnable() {public void run() {System.out.println("線程名是:" + Thread.currentThread().getName());}});}}
}
Thread 和 Runnable 兩種開發線程的區別
- 繼承Thread類,不能實現多個線程共享同一個實例資源,實現Runnable接口多個線程可以共享同一個資源。
- 繼承Thread類不能繼承其他類,有單繼承局限性,實現Runnable接,還可以繼承其他類。
Runnable 和 Callable 區別
- Runnable接口和Callable接口都可以用來創建新線程,實現Runnablel的時候,需要實現run方法;實現Callable 接口的話,需要實現call方法。
- Runnable的run方法無返回值,Callable的call方法有返回值,類型為Object。
- Callable中可以夠拋出checked exception,而Runnable不可以。
FutureTask 和 Callable 示例
Callable<String> callable = () -> {System.out.println("Entered Callable");Thread.sleep(2000);return "Hello from Callable";
};
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("Do something else while callable is getting executed");
System.out.println("Retrieved:" + futureTask.get());
線程池和Callable的示例
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> callable = () -> {System.out.println("Entered Callable");Thread.sleep(2000);return "Hello from Callable";
};
System.out.println("Submitting Callable");
Future<String> future = executor.submit(callable);
System.out.println("Do something else while callable is getting executed");
System.out.println("Retrieved:" + future.get());
executor.shutdown();
3、線程有幾種狀態
- 新建狀態(new):創建一個線程對象。
- 就緒狀態(Runnable):線程對象創建之后,調用start方法,就緒狀態的線程只處于等待CPU的使用權,變為可運行。
- 運行狀態(Running): 就緒狀態的線程,獲取到了CPU資源,執行程序代碼。
- 阻塞狀態(Blocked): 等待阻塞(線程執行了一個對象的wait方法,進入阻塞狀態,只有等到其他線程執行了該對象的notify或notifyAll方法,才可能將其喚醒)、線程阻塞(線程獲取synchronized同步鎖失敗因為鎖被其它線程鎖占用,它會進入同步阻塞狀態)、其它阻塞(通過調用線程的sleep或join或發出了IO請求時,線程就會進入阻塞狀態。當sleep超時或join等待線程終止或超時或IO處理完畢時,線程重新轉入就緒狀態)。
- 死亡狀態(Dead):線程任務執行結束,即run方法結束,該線程對象就會被垃圾回收,線程對象即為死亡狀態。
線程是如何被調度的
進程是分配資源的基本單元,線程是CPU調度的基本單元。這里所說的調度指的就是給其分配CPU時間片,讓其
執行任務。
run方法和start方法區別
我們創建好線程之后,想要啟動這個線程,則需要調用其start方法。所以,start方法是啟動一個線程的入口。如果在創建好線程之后,直接調用其run方法,那么就會在單線程中直接運行run方法,不會起到多線程的效果。
WAITING 和 TIMED WAIT 的區別
WAITING是等待狀態,在Java中,調用wait方法時,線程會進入到WAITING 狀態,而TIMED WAITING是超時等
待狀態,當線程執行sleep方法時,線程會進入TIMED WAIT狀態。
sleep和wait區別
- sleep方法可以在任何地方使用,而wait方法則只能在同步方法或同步塊中使用。
- wait方法會釋放對象鎖,但sleep方法不會。
- wait、notify、notifyAll針對的目標都是對象,所以把他們定義在Object類中。而sleep不需要釋放鎖,所以他是Thread類中的一個方法。
- wait的線程會進入到WAITING狀態,直到被喚醒,sleep的線程會進入到TIMED WAITING狀態,等到指定時間之后會再嘗試獲取CPU時間片。
notity和notityAll區別
當一個線程進入wait之后,就必須等其他線程notify或者notifyAll才會從等待隊列中被移出。使用notifyAll可以喚醒所有處于wait狀態的線程,使其重新進入鎖的爭奪隊列中,而notify只能喚醒一個。被notify/notifyAll喚醒的線程,只是表示他們可以競爭鎖了,競爭到鎖之后才有機會被CPU調度。
Thread.sleep(0)的作用是什么
sleep方法需要指定一個時間,表示sleep的毫秒數,但是有的時候我們會見到Thread.sleep(0)這種用法其實就是讓當前線程釋放一下CPU時間片,然后重新開始爭搶。
線程優先級
雖然Java線程調度是系統自動完成的,但是我們還是可以"建議”系統給某些線程多分配一點執行時間,另外的一些線程則可以少分配一點,這項操作可以通過設置線程優先級來完成。
方法 | 描述 |
---|---|
static Thread currentThread() | 獲取當前線程對象(線程名稱, 線程優先級, 線程所屬線程組) |
String getName() | 獲取當前線程對象 |
viod set(String name) | 設置線程名稱 |
int getId() | 獲取線程id |
int getPriority(int i) | 設置線程級別 |
static Thread currentThread() | 獲取當前線程對象 |
void setDaemon(boolean bo) | 設置一個線程為守護(后臺)線程 |
boolean isDaemon() | 獲取守護線程是true還是false |
static native void sleep(long millis) | 設置休眠(單位毫秒) |
static native void yield() | 當前線程放棄時間片 |
void join() | 等待該線程終止 |
void wait() | 設置當前線程等待阻塞狀態 |
void notify() | 喚醒正處于等待狀態的線程 |
void notifyAll() | 喚醒所有處于等待狀態的線程 |
4、什么是上下文切換
多線程編程中一般線程的個數都大于 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 采取的策略是為每個線程分配時間片并輪轉的形式。
一個線程被剝奪CPU的使用權就是 “切出”,一個線程獲得CPU的使用權就是 “切入”,這種切入切出過程就是上線文。當一個線程的時間片用完的時候就會重新處于就緒狀態讓給其他線程使用,這個過程就屬于一次上下文切換。
在多線程中,上下文切換的開銷比單線程大,因為在多線程中,需要保存和恢復更多的上下文信息。過多上下文切換會降低系統的運行效率,因此需要盡可能減少上下文切換的次數。
減少上下文的切換的方式
- 減少線程數:可以通過合理的線程池也管理來減少線程的創建和銷毀,線程數不是越多誠好,合理的線程數可以避免線程過多導致上下文切換。
- 使用CAS算法:CAS算法可以避免線程的阻塞和喚醒操作,從而減少上下文切換。
- 合理地使用鎖:在使用鎖的過程中,需要避免過多地使用同步塊或同步方法,盡量縮小同步塊或同步方法的范
圍,從而減少線程的等待時間,避免上下文切換的發生。
5、什么是守護線程,和普通線程有什么區別
在Java中有兩類線程:User Thread用戶線程、Daemon Thread守護線程。用戶線程一般用戶執行用戶級任務,而守護線程也就是后臺線程,一般用來執行后臺任務,守護線程最典型的應用就是GC垃圾回收器。
這兩種線程其實是沒有什么區別的,唯一的區別就是虛擬機在所有用戶線程都結束后就會退出,而不會等守護線程執行完。
Thread t1 = new Thread();
t1.setDaemon(true);
System.out.println(t1.isDaemon());
6、什么是線程池,如何實現的
顧名思義,線程池就是管理一系列線程的資源池。當有任務要處理時,直接從線程池中獲取線程來處理,處理完之后線程并不會立即被銷毀,而是等待下一個任務。
為什么要使用線程池
- 減少了創建和銷毀線程的次數,每個工作線程都可以被重復使用或利用,可以并發執行多個任務。
- 可以根據系統的承受能力,調整線程池中的工作數目,防止消耗過多的內存而使服務器宕機。
線程池的使用
(1)通過 Executor 框架的工具類 Executors 來創建
Executors的創建線程池的方法,創建出來的線程池都實現了ExecutorService:接口。常用方法有以下幾個:
- newFiexedThreadPool(int Threads)創建固定數目線程的線程池。
- newSingleThreadExecutor()創建一個單線程化的Executor。
- newCachedThreadPool0:創建一個可緩存的線程池,調用execute將重用以前構造的線程。如果沒有可用的線程,則創建一個新線程并添加到池中。終止并從緩存中移除那些已有60秒鐘未被使用的線程。
- newScheduledThreadPool(int corePoolSize)創建一個支持定時及周期性的任務執行的線程池,多數情況下可用來替代Timer類。
(2)通過ThreadPoolExecutor構造函數來創建
ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
(3)ThreadPoolTaskExecutor是對ThreadPoolExecutor進行了封裝處理
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 最大線程數
executor.setCorePoolSize(corePoolSize);
// 核心線程數
executor.setMaxPoolSize(maxPoolSize);
// 任務隊列的大小
executor.setQueueCapacity(queueCapacity);
// 線程池名的前綴
executor.setThreadNamePrefix(namePrefix);
7、Executor和Executors的區別
Executors 工具類的不同方法按照我們的需求創建了不同的線程池,來滿足業務的需求。
Executor 接口對象能執行我們的線程任務。ExecutorService接口繼承了Executor接口并進行了擴展,提供了更多的方法我們能獲得任務執行的狀態并且可以獲取任務的返回值。
使用ThreadPoolExecutor 可以創建自定義線程池。Future 表示異步計算的結果,他提供了檢查計算是否完成的方法,以等待計算的完成,并可以使用get()方法獲取計算的結果。
8、線程池處理任務的流程
線程池工作流程
- 核心線程:
- 初始時線程池為空。
- 當有任務到來時,使用核心線程處理。
- 如果核心線程不足,則創建新線程,直到達到 corePoolSize。
- 任務隊列:
- 如果核心線程都在忙,新任務會被放入 workQueue。
- 隊列滿時,會創建臨時線程,直到達到 maximumPoolSize。
- 臨時線程:
- 當核心線程和隊列都滿時,創建臨時線程處理新任務。
- 臨時線程空閑超過 keepAliveTime 后會被回收。
- 拒絕策略:
當線程池和隊列都滿時,觸發 handler 拒絕策略。
線程池的拒絕策略有那些
- AbortPolicy(默認),直接拋出一個類型為 RejectedExecutionException 的 RuntimeException異常阻止系統的正常運行。
- DiscardPolicy:直接丟棄任務,不給予任何處理也不拋出異常。如果允許任務丟失的話,這是最好的方案。
- DiscardOldestPolicy,拋棄隊列中等待時間最長的任務,然后把當前任務加入隊列中嘗試再次提交任務。
- CallerRunsPolicy:"調用者運行"一種調節機制,該策略既不會拋棄任務也不會拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。
線程池常用的阻塞隊列有哪些
9、線程數設定成多少更合適
一般情況下,需要根據你的任務情況來設置線程數,任務可能是兩種類型,分別是CPU密集型和IO密集型。
- 如果是CPU密集型應用,則線程池大小設置為N+1
- 如果是IO密集型應用,則線程池大小設置為2N+1
CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。CPU密集任務只有在真正的多核CPU上才可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程該任務都不可能得到加速,因為CPU總的運算能力就那些。IO包括數據庫交互,文件上傳下載,網絡傳輸等。
10、執行execute方法和submit方法的區別
- 如果任務不需要返回結果,且不需要處理異常,可以使用 execute()。日志記錄、異步通知等。
- 如果任務需要返回結果,或者需要處理異常,建議使用 submit()。計算任務、需要返回值的任務等。
11、什么是ThreadLocal,如何實現的
ThreadLocal 是用來解決java多線程程序中并發問題的一種途徑,是java中的一個線程本地變量,在多線程環境下維護每個下線程的獨立數據副本,通過為每一個線程創建一份共享變量的副本來保證各個線程之間的變量的訪問和修改互相不影響。
ThreadLocal有四個方法,分別為:
- initialValue:返回此線程局部變量的初始值。
- get:返回此線程局部變量的當前線程副本中的值。如果線程第一次調用該方法,則創建并初始化此副本。
- set:將此線程局部變量的當前線程副本中的值設置為指定值。許多應用程序不需要這項功能,它們只依賴于
initialValue方法來設置線程局部變量的值。 - remove:移除此線程局部變量的值。
ThreadLocal原理
public void set(T value) {// 獲取當前請求的線程Thread t = Thread.currentThread();// 取出 Thread 類內部的 threadLocals 變量(哈希表結構)ThreadLocalMap map = getMap(t);if (map != null)// 將需要存儲的值放入到這個哈希表中map.set(this, value);elsecreateMap(t, value);
}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
通過上面這些內容,我們足以通過猜測得出結論:最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal為 key ,Object 對象為 value 的鍵值對。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//......
}
比如我們在同一個線程中聲明了兩個 ThreadLocal 對象的話, Thread內部都是使用僅有的那個ThreadLocalMap 存放數據的,ThreadLocalMap的 key 就是 ThreadLocal對象,value 就是 ThreadLocal 對象調用set方法設置的值。
ThreadLocal 數據結構如下圖所示:
ThreadLocal中用于保存線程的獨有變量的數據結構是一個內部類:ThreadLocalMap,也是k-v結構key就是當前的ThreadLoacaly對象,而v就是我們想要保存的值。
上圖中基本描述出了Thread、ThreadLocalMapl以及ThreadLocal三者之間的包含關系。
ThreadLocal 內存泄露問題
了解了ThreadLocal的基本原理之后,我們把上面的圖補全,從堆棧的視角整體看一下他們之間的引用關系。
ThreadLocal對象,是有兩個引用的,一個是棧上的ThreadLocal引用一個是ThreadLocalMap中的Key對他的引用。
那么,假如,棧上的ThreadLocal引用不在使用了,即方法結束后這個對象引用就不再用了,那么,ThreadLocal
對象因為還有一條引用鏈在,所以就會導致他無法被回收,久而久之可能就會對導致OOM。這就是我們所說的ThreadLocal的內存泄露問題。
原因是 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強引用。所以,如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。他的生命周期是和Thread 一樣的,也就是說,只要這個Thread還在,這個對象就無法被回收。
那么,什么情況下,Thread會一直在呢?那就是線程池。
在線程池中,重復利用線程的時候,就會導致這個引用一直在,而value就一直無法被回收。那么如何解決呢?
ThreadLocalMap底層使用數組來保存元素,使用"線性探測法”來解決hash沖突的,在每次調用ThreadLocal的
get、set、remove等方法的時候,內部會實際調用ThreadLocalMap的get、set、remove等操作。而ThreadLocalMap的每次get、set、remove,都會清理過期的Entry。.
所以,當我們在一個ThreadLocall用完之后,手動調用一下remove,就可以在下一次GC的時候,把Entryi清理
掉。
12、父子線程之間怎么共享數據
當我們在同一個線程中,想要共享變量的話,是可以直接使用ThreadLocal的,但是如果在父子線程之間,共享變
量,ThreadLocal就不行了。
public class TestYang {public static ThreadLocal<Integer> sharedData = new ThreadLocal<>();public static void main(String[] args) {sharedData.set(0);// 主線程設置 0MyThread thread = new MyThread(); // 定義子線程thread.start();// 開啟子線程sharedData.set(sharedData.get() + 1); // 主線程設置 1System.out.println("sharedData in main thread:" + sharedData.get());// 獲取主線程的值 1}static class MyThread extends Thread {@Overridepublic void run() {System.out.println("sharedData in child thread:" + sharedData.get());// nullsharedData.set(sharedData.get() + 1);System.out.println("sharedData in child thread after increment:" + sharedData.get());}}
}
因為ThreadLocal變量是為每個線程提供了獨立的副本,因比不同線程之間只能訪問它們自己的副本。那么,想要實現數據共享,主要有兩個辦法,第一個是自己傳遞,第二個是借助InheritableThreadLocal
InheritableThreadLocal
與ThreadLocal不同,InheritableThreadLocal可以在子線程中繼承父線程中的值。在創建子線程時,子線程將復制父線程中的InheritableThreadLocal變量。我們把開頭的示例中ThreadLocal改成InheritableThreadLocal就可以了:
public class TestYang {public static InheritableThreadLocal<Integer> sharedData = new InheritableThreadLocal<>();public static void main(String[] args) {sharedData.set(0);// 主線程設置 0MyThread thread = new MyThread(); // 定義子線程thread.start();// 開啟子線程sharedData.set(sharedData.get() + 1); // 主線程設置 1System.out.println("sharedData in main thread:" + sharedData.get());// 獲取主線程的值 1}static class MyThread extends Thread {@Overridepublic void run() {System.out.println("sharedData in child thread:" + sharedData.get());// 0sharedData.set(sharedData.get() + 1);// 1System.out.println("sharedData in child thread after increment:" + sharedData.get());// 1}}
}
13、線程同步的方式有哪些
線程同步指的就是讓多個線程之間按照順序訪問同一個共享資源,避免因為并發沖突導致的問題,主要有以下幾種
方式:
-
synchronized:Java中最基本的線程同步機制,可以修飾代碼塊或方法,保證同一時間只有一個線程訪問該代碼塊或方法,其他線程需要等待鎖的釋放。
-
ReentrantLock:與synchronized關鍵字類似,也可以保證同一時間只有一個線程訪問共享資源,但是更靈活,支持公平鎖、可中斷鎖、多個條件變量等功能。
-
CountDownLatch:允許一個或多個線程等待其他線程執行完畢之后再執行,用于線程之間的協調和通信。
-
Semaphore:允許多個線程同時訪問共享資源,但是限制訪問的線程數量。用于控制并發訪問的線程數量,避免系統資源被過度占用。
-
CyclicBarrier類:允許多個線程在一個柵欄處等待,直到所有線程都到達柵欄位置之后,才會繼續執行。
-
Phaser:與CyclicBarrier類似,也是一種多線程同步工具,但是支特更靈活的柵欄操作,可以動態地注冊和注銷參與者,并可以控制各個參與者的到達和離開。
14、synchronized 是怎么實現的
① synchronized 用法
synchronized 關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。
synchronized 關鍵字最主要的三種使用方式:
- 修飾實例方法 (鎖當前對象實例)
給當前對象實例加鎖,進入同步代碼前要獲得 當前對象實例的鎖 。
synchronized void method() {//業務代碼
}
- 修飾靜態方法(鎖當前類)
給當前類加鎖,會作用于類的所有對象實例 ,進入同步代碼前要獲得 當前 class 的鎖。
這是因為靜態成員不屬于任何一個實例對象,歸整個類所有,不依賴于類的特定實例,被類的所有實例共享。
synchronized void staic method() {//業務代碼
}
- 修飾代碼塊 (鎖指定對象/類)
對括號里指定的對象/類加鎖:
- synchronized(object) 表示進入同步代碼庫前要獲得 給定對象的鎖。
- synchronized(類.class) 表示進入同步代碼前要獲得 給定 Class 的鎖
synchronized(this) {//業務代碼
}
② synchronized 同步語句塊的情況
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 對象監視器 monitor 的持有權。
③ synchronized 修飾方法的的情況
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法。
JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。如果是實例方法,JVM 會嘗試獲取實例對象的鎖。如果是靜態方法,JVM 會嘗試獲取當前 class 的鎖。
15、synchronized 的鎖升級過程是怎樣的
在JDK1.6之后,synchronized鎖的實現發生了一些變化,引入了"偏向鎖”、"輕量級鎖”和"重量級鎖”三種不同的狀態,用來適應不同場景下的鎖競爭情況。
- 無鎖狀態:當對象鎖被創建出來時,在線程獲得該對象鎖之前,對象處于無鎖狀態。
- 偏向鎖:偏向鎖是指在只有一個線程訪問對象的情況下,該線程不需要使用同步操作就可以訪問對象。在這種情況下,如果其他線程訪問該對象,會先檢查該對象的偏向鎖標識,如果和自己的線程ID相同,則直接獲取鎖。如果不同,就是發生了鎖競爭,則該對象的鎖狀態就會升級到輕量級鎖狀態。
- 輕量級鎖:就是CAS,它會讓線程線程在那里自旋,在OpenJDK8中,輕量級鎖的自旋默認是開啟的,最多自旋15次,如果15次自旋后仍然沒有獲取到鎖,就會升級為重量級鎖。
- 重量級鎖:如果一個線程想要獲取該對象的鎖,則需要先進入等待隊列,等待該鎖被釋放。當鎖被釋放時,JVM會從等待隊列中選擇一個線程喚醒,并將該線程的狀態設置為“就緒”狀態,然后等待該線程重新獲取該對象的鎖。
16、什么是鎖消除和鎖粗化
- 鎖消除
比如StringBuffer的append方法,因為append方法需要判斷對象是否被占用,而如果代碼不存在鎖競爭,那么這部分的性能消耗是無意義的。于是虛擬機在即時編譯的時候就會將上面的代碼進行優化,也就是鎖消除。
@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;
}
- 鎖粗化
當發現一系列連續的操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作出現在循環體中的時候,會將加鎖同
步的范圍散(粗化)到整個操作序列的外部。
for(inti=0;i<100000;i++){synchronized(this){do();}
}
會被粗化成:
synchronized(this){for(inti=0;i<100000;i++){do();}
}
17、synchronized 和 reentrantLock 區別
相同點是都是可重入鎖,不同點如下
- synchronized 可用來修飾普通方法、靜態方法和代碼塊,而 ReentrantLock 只能用在代碼塊上。
- synchronized 會自動加鎖和釋放鎖,當進入 synchronized 修飾的代碼塊之后會自動加鎖,當離開 synchronized 的代碼段之后會自動釋放鎖,而 ReentrantLock 需要手動加鎖和釋放鎖。
- synchronized 是 JVM 層面通過監視器實現的,而 ReentrantLock 是通過 AQS 程序級別的 API 實現。
- synchronized 屬于非公平鎖,而 ReentrantLock 既可以是公平鎖也可以是非公平鎖。默認非公平鎖。
公平鎖和非公平鎖有什么區別
公平鎖:每個線程獲取鎖的順序是按照線程訪問鎖的先后順序獲取的,最前面的線程總是最先獲取到鎖。非公平鎖:每個線程獲取鎖的順序是隨機的,并不會遵循先來先得的規則,所有線程會競爭獲取鎖。
在 Java 語言中,鎖 synchronized 和 ReentrantLock 默認都是非公平鎖,當然我們在創建 ReentrantLock 時,可以手動指定其為公平鎖,但 synchronized 只能為非公平鎖。
怎么創建公平鎖
new ReentrantLock()默認創建的為非公平鎖,如果要創建公平鎖可以使用new ReentrantLock(true)。
lock()和lockInterruptibly()的區別
lock和lockInterruptibly的區別在于獲取鎖的途中如果所在的線程中斷,Iock會忽略異常繼續等待獲取鎖,而lockInterruptibly則會拋出InterruptedException異常。
tryLock()
tryLock(5,TimeUnit.SECONDS)表示獲取鎖的最大等待時間為5秒,期間會一直嘗試獲取,而不是等待5秒之后再去獲取鎖。
reentrantLock 底層原理
ReentrantLock asd = new ReentrantLock();
ReentrantLock 鎖是一個輕量級鎖,底層其實就是用自旋鎖實現的,當我們調用 lock 方法的時候,在內部其實調用了 Sync.lock 方法,Sync 繼承了 AQS,AQS 內部有一個 volatile 類型的 state 屬性,實際上多線程對鎖的競爭體現在對 state 值寫入的競爭。一旦 state 從 0 變為 1,代表有線程已經競爭到鎖,那么其它線程則進入等待隊列。通過CAS修改了 state,修改成功標志自己成功獲取鎖。如果CAS失敗的話,調用 acquire 方法
AQS 的 lock 有兩個實現方法,一個在 ReentrantLock 非公平鎖,一個在公平鎖,非公平鎖調用 lock 方法通過 CAS 去更新 AQS 的 state 的值(鎖的狀態值),更新成功就是獲得鎖可以執行。更新不成功就將沒獲得鎖的線程放入鏈表尾部,自旋等待狀態被釋放,釋放了,用CAS獲得鎖
// 所以在底層調用的其實是AQS的lock()方法,
asd.lock();
18、synchronized 和 Lock 有什么區別
- synchronized 可以給類,方法,代碼塊加鎖,而 lock 只能給代碼塊加鎖。
- synchronized 不需要手動獲取鎖和釋放鎖,使用簡單,發生異常會自動釋放鎖,不會造成死鎖,而 lock 需要自己加鎖和釋放鎖,如果使用不當沒有 unLock()去釋放鎖就會造成死鎖。
- 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。
19、CountDownLatch、CyclicBarrier、Semaphore
CountDownLatch、CyclicBarrier、Semaphore都是ava并發庫中的同步輔助類,它們都可以用來協調多個線程
之間的執行。
- CountDownLatch是一個計數器,它允許一個或多個線程等待其他線程完成操作。它通常用來實現一個線程等待其他多個線程完成操作之后再繼續執行的操作。
- CyclicBarrier是一個同步屏障,它允許多個線程相互等待,直到到達某個公共屏障點,才能繼續執行。它通常用來實現多個線程在同一個屏障處等待,然后再一起繼續執行的操作。
- Semaphore是一個計數信號量,它允許多個線程同時訪問共享資源,并通過計數器來控制訪方問數量。它通常用來實現一個線程需要等待獲取一個許可證才能訪問共享資源,或者需要釋放一個許可證才能完成操作的操作。
CountDownLatch和CyclicBarrier區別
- CountDownLatch的計數器只能使用一次,CyclicBarrier的計數器可以使用reset方法進行重置并且可以循環使用。
- CountDownLatch主要實現1個或n個線程需要等待其他線程完成某項操作之后才能繼續往下執行,描述的是1個或n個線程等待其他線程的關系,CyclicBarrier主要實現了多個線程之間相互等待,直到所有線程都滿足了條件之后才能繼續執行后續的操作,描述的是各個線程內部相互等待的關系。
- CyclicBarrier能夠處理更復雜的場景,如果計算發生錯誤可以重置計數器讓線程重新執行一次
- CyclicBarrier中提供了很多有用的方法,比如可以通過getNumber Waiting方法獲取阻塞的線程數量,通過isBroken方法判斷阻塞的線程是否被中斷
有三個線程T1、T2、T3如何保證順序執行
想要讓三個線程依次執行,并且嚴格按照T1,T2,T3的順序的話,主要就是想辦法讓三個線程之間可以通信、或者可以排隊。
想讓多個線程之間可以通信,可以通過join方法實現,還可以通過CountDownLatch、CyclicBarrier和Semaphore來實現通信。想要讓線程之間排隊的話,可以通過線程池或者CompletableFuturel的方式來實現。
join
final Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "is Running.");}}, "T1");final Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {try {thread1.join();} catch (InterruptedException e) {System.out.println("join thread1 failed");}System.out.println(Thread.currentThread().getName() + "is Running.");}}, "T2");Thread thread3 = new Thread(new Runnable() {@Overridepublic void run() {try {thread2.join();} catch (InterruptedException e) {System.out.println("join thread1 failed");}System.out.println(Thread.currentThread().getName() + "is Running.");}}, "T3");thread3.start();thread2.start();thread1.start();
CountDownLatch
AQS中的CountDownLatch并發工具類,通過它可以阻塞當前線程,也就是說能夠實現一個線程或者多個線程的一直等待,直到其他線程執行的操作完成,使用一個給定的計數器進行初始化,該技術器的操作是原子操作,即同時只能有一個線程操作該計數器,調用該類的await方法的線程會一直阻塞直到其他線程調用該類的countDown方法使當前計數器的值變為0為止,每次調用該類的countDown方法當前計數器的值都會減1,當計數器的值減為0的時候所欲因調用await方法而出處于等待狀態的線程就會繼續往下執行,這種操作只能出現一次,因為該類的計數器不能被重置,如果需要一個可以重置的計數次數的版本可以考慮使用CyclicBarrier類,CountDownLatch支持給定時間等待,超過一定時間不再等待,使用時只需要在CountDownLatch方法中傳入需要等待的時間即可,使用場景:在程序執行需要等待某個條件完成后才能繼續執行后續的操作,典型的應用為并行計算,當某個處理的運算量很大時可以將該運算拆分成多個子任務,等待所有的子任務都完成后,父任務再拿到所有子任務的運算計算結果匯總。
// 創建CountDownLatch對象,用來做線程通信CountDownLatch latch = new CountDownLatch(1);CountDownLatch latch2 = new CountDownLatch(1);CountDownLatch latch3 = new CountDownLatch(1);// 創建并啟動線程T1Thread t1 = new Thread(new MyThread(latch), "T1");t1.start();// 等待線程T1執行完latch.await();// 創建并啟動線程T2Thread t2 = new Thread(new MyThread(latch2), "T2");t2.start();// 等待線程T2執行完latch2.await();// 創建并啟動線程T3Thread t3 = new Thread(new MyThread(latch3), "T3");t3.start();// 等待線程T3執行完latch3.await();}
}class MyThread implements Runnable {private CountDownLatch latch;public MyThread(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {try {// 模擬執行任務Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "is Running.");} catch (InterruptedException e) {e.printStackTrace();} finally {// 完成一個線程,計數器減1latch.countDown();}}
CyclicBarrier
允許一組線程相互等待直到到達某個公共的屏障點,通過它可以完成多個線程之間的相互等待,只有當每個線程都準備就緒后才能各自繼續往下執行后面的操作,與CountDownLatch有相似的地方都是使用計數器實現,當某個線程調用了CyclicBarrier的await方法后,該線程就進入了等待狀態,而且計數器執行加1操作,當計數器的值達到了設置的初始值調用await方法進入等待狀態的線程會被喚醒繼續執行各自后續的操作,CyclicBarrier在釋放等待線程后可以重復使用,所以,CyclicBarrier又被稱為循環屏障,使用場景:CyclicBarrier可以用于多線程計算數據,最后合并計算結果的場景。
// CyclicBarrier,用來做線程通信CyclicBarrier barrier = new CyclicBarrier(2);// 創建并啟動線程T1Thread t1 = new Thread(new MyThread(barrier), "T1");t1.start();// 等待線程T1執行完barrier.await();// 創建并啟動線程T2Thread t2 = new Thread(new MyThread(barrier), "T2");t2.start();// 等待線程T2執行完barrier.await();// 創建并啟動線程T3Thread t3 = new Thread(new MyThread(barrier), "T3");t3.start();// 等待線程T3執行完barrier.await();}
}class MyThread implements Runnable {private CyclicBarrier barrier;public MyThread(CyclicBarrier barrier) {this.barrier = barrier;}@Overridepublic void run() {try {// 模擬執行任務Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "is Running.");} catch (InterruptedException e) {e.printStackTrace();} finally {// 等待其他線程完成try {barrier.await();} catch (Exception e) {e.printStackTrace();}}}
Semaphore
控制同一時間并發線程的數量,能夠完成對于信號量的控制,可以控制某個資源同時訪問的個數,提供了兩個核心方法acquire和release方法,acquire方法表示獲取一個許可,如果沒有則等待,release方法則是在操作完成后釋放對應的許可,Semaphore維護了當前訪問的個數,通過提供同步機制來控制同時訪問的個數,Semaphore可以實現有限大小的鏈表,使用場景:常用于僅能提供有限訪問資源的業務場景,比如數據庫連接數。業務請求并發太高已經超過了系統并發處理的閾值,對超過上限的請求進行丟棄處理。
// Semaphore,用來做線程通信Semaphore semaphore = new Semaphore(1);// 創建并啟動線程T1Thread t1 = new Thread(new MyThread(semaphore), "T1");t1.start();// 等待線程T1執行完semaphore.acquire();// 創建并啟動線程T2Thread t2 = new Thread(new MyThread(semaphore), "T2");t2.start();// 等待線程T2執行完semaphore.acquire();// 創建并啟動線程T3Thread t3 = new Thread(new MyThread(semaphore), "T3");t3.start();// 等待線程T3執行完semaphore.acquire();}
}class MyThread implements Runnable {private Semaphore semaphore;public MyThread(Semaphore semaphore) {this.semaphore = semaphore;}@Overridepublic void run() {try {// 模擬執行任務Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "is Running.");} catch (InterruptedException e) {e.printStackTrace();} finally {// 釋放許可證,表示完成以一個線程semaphore.release();}}
使用線程池
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {// 創建線程池ExecutorService executor = Executors.newSingleThreadExecutor();// 創建并啟動線程T1executor.submit(new MyThread("T1"));// 創建并啟動線程T2executor.submit(new MyThread("T2"));// 創建并啟動線程T3executor.submit(new MyThread("T3"));// 關閉線程池executor.shutdown();}
}class MyThread implements Runnable {private String name;public MyThread(String name) {this.name = name;}@Overridepublic void run() {try {// 模擬執行任務Thread.sleep(1000);System.out.println(name + "is Running.");} catch (InterruptedException e) {e.printStackTrace();}}
}
CompletableFuture
public class TestYang {public static void main(String[] args) throws ExecutionException, InterruptedException {//創建CompletableFuture對象CompletableFuture<Void> future = CompletableFuture.runAsync(new MyThread("T1")).thenRun(new MyThread("T2")).thenRun(new MyThread("T3"));future.get();}
}class MyThread implements Runnable {private String name;public MyThread(String name) {this.name = name;}@Overridepublic void run() {try {// 模擬執行任務Thread.sleep(1000);System.out.println(name + "is Running.");} catch (InterruptedException e) {e.printStackTrace();}}
}
20、volatile 是如何保證可見性和有序性的不能保證原子性的
- volatile和可見性
對于volatile變量,當對volatile變量進行寫操作的時候,JVM會向處理器發送一條lock前綴的指令,將這個緩存中
的變量回寫到系統主存中。
所以,如果一個變量被volatile所修飾的話,在每次數據變化之后,其值都會被強制刷入主存。而其他處理器的緩
存由于遵守了緩存一致性協議,也會把這個變量的值從主存加載到自己的緩存中。這就保證了一個volatile在并發
編程中,其值在多個緩存中是可見的。
- volatile和有序性
volatile除了可以保證數據的可見性之外,還有一個強大的功能,那就是他可以禁止指令重排優化等。
普通的變量僅僅會保證在該方法的執行過程中所依賴的賦值結果的地方都能獲得正確的結果,而不能保證變量的賦
值操作的順序與程序代碼中的執行順序一致。
volatile是通過內存屏障來禁止指令重排的,這就保證了代碼的程序會嚴格按照代碼的先后順序執行。
- volatile和原子性
為什么volatile不能保證原子性呢?因為他不是鎖,他沒做任何可以保證原子性的處理。當然就不能保證原子性了。
我們通過下面的代碼即可證明:
public class VolatoleAtomicityDemo {public volatile static int inc = 0;public void increase() {inc++;}public static void main(String[] args) throws InterruptedException {ExecutorService threadPool = Executors.newFixedThreadPool(5);VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo();for (int i = 0; i < 5; i++) {threadPool.execute(() -> {for (int j = 0; j < 500; j++) {volatoleAtomicityDemo.increase();}});}// 等待1.5秒,保證上面程序執行完成Thread.sleep(1500);System.out.println(inc);threadPool.shutdown();}
}
正常情況下,運行上面的代碼理應輸出 2500。但你真正運行了上面的代碼之后,你會發現每次輸出結果都小于 2500。
為什么會出現這種情況呢?不是說好了,volatile 可以保證變量的可見性嘛!
也就是說,如果 volatile 能保證 inc++ 操作的原子性的話。每個線程中對 inc 變量自增完之后,其他線程可以立即看到修改后的值。5 個線程分別進行了 500 次操作,那么最終 inc 的值應該是 5*500=2500。很多人會誤認為自增操作 inc++ 是原子性的,實際上,inc++ 其實是一個復合操作,包括三步:
- 讀取 inc 的值。
- 對 inc 加 1。
- 將 inc 的值寫回內存。
volatile 是無法保證這三個操作是具有原子性的,有可能導致下面這種情況出現:
- 線程 1 對 inc 進行讀取操作之后,還未對其進行修改。線程 2 又讀取了 inc的值并對其進行修改(+1),再將inc 的值寫回內存。
- 線程 2 操作完畢后,線程 1 對 inc的值進行修改(+1),再將inc 的值寫回內存。
這也就導致兩個線程分別對 inc 進行了一次自增操作后,inc 實際上只增加了 1。其實,如果想要保證上面的代碼運行正確也非常簡單,利用 synchronized 、Lock或者AtomicInteger都可以。
使用 synchronized 改進:
public synchronized void increase() {inc++;
}
使用 AtomicInteger 改進:
public AtomicInteger inc = new AtomicInteger();public void increase() {inc.getAndIncrement();
}
使用 ReentrantLock 改進:
Lock lock = new ReentrantLock();
public void increase() {lock.lock();try {inc++;} finally {lock.unlock();}
}
21、synchronized 和 volatile 有什么區別
synchronized 關鍵字和 volatile 關鍵字是兩個互補的存在,而不是對立的存在!
- volatile 關鍵字是線程同步的輕量級實現,所以 volatile性能肯定比synchronized關鍵字要好 。但是 volatile 關鍵字只能用于變量而 synchronized 關鍵字可以修飾方法以及代碼塊。
- volatile 關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized 關鍵字兩者都能保證。
- volatile關鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized 關鍵字解決的是多個線程之間訪問資源的同步性。
22、什么是死鎖,如何解決
線程死鎖(Thread Deadlock) 是多線程編程中的一種常見問題,指的是兩個或多個線程在執行過程中,因為爭奪資源而造成的一種互相等待的現象,導致這些線程都無法繼續執行下去。
產生死鎖的四個必要條件
- 互斥條件:一個資源每次只能被一個進程使用。
- 占有且等待:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不可強行占有:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
public class DeadlockExample {private static final Object resource1 = new Object();private static final Object resource2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (resource1) {System.out.println("Thread 1: Holding resource 1...");try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println("Thread 1: Waiting for resource 2...");synchronized (resource2) {System.out.println("Thread 1: Holding resource 1 and 2...");}}});Thread thread2 = new Thread(() -> {synchronized (resource2) {System.out.println("Thread 2: Holding resource 2...");try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println("Thread 2: Waiting for resource 1...");synchronized (resource1) {System.out.println("Thread 2: Holding resource 1 and 2...");}}});thread1.start();thread2.start();}
}
如何解除死鎖
- 破壞不可搶占:設置優先級,使優先級高的可以搶占資源。
- 破壞循環等待:保證多個進程(線程)的執行順序相同即可避免循環等待。
數據庫死鎖的發生
在數據庫中,如果有多個事務并發執行,也是可能發生死鎖的。當事務1持有資源八的鎖,但是嘗試獲取資源B的
鎖,而事務2持有資源B的鎖,嘗試獲取資源A的鎖的時候,這時候就會發生死鎖的情況發生死鎖時,會發生如下異常:
Error updating database.Cause:ERR-CODE:[TDDL-4614][ERR_EXECUTE_ON_MYSQL]
Deadlock found when trying to get lock;
23、介紹一下 Atomic 原子類
Atomic 原子類 是 Java 并發包(java.util.concurrent.atomic)中提供的一組類,用于在多線程環境下實現無鎖的線程安全操作。它們通過硬件級別的原子操作(如 CAS,Compare-And-Swap)來保證操作的原子性,避免了使用鎖帶來的性能開銷。
- Atomic 原子類的作用
- 線程安全:
- 提供原子操作,確保多線程環境下的數據一致性。
- 高性能:
- 使用無鎖機制(如 CAS),避免了鎖競爭和上下文切換的開銷。
- 簡化編程:
- 提供簡單易用的 API,避免手動實現復雜的同步邏輯。
- Atomic 原子類的核心原理
- CAS 操作:
- 比較當前值與預期值,如果相等,則更新為新值;否則,不做任何操作。
- CAS 是硬件級別的原子操作,由 CPU 直接支持。
- CAS 的三大問題:
- ABA 問題:值從 A 變為 B 又變回 A,CAS 無法感知中間變化。解決方法:使用 AtomicStampedReference 或 AtomicMarkableReference。
- 循環時間長開銷大:如果 CAS 操作失敗,會不斷重試,導致 CPU 開銷增加。
- 只能保證一個變量的原子性:如果需要保證多個變量的原子性,需要使用鎖或其他機制。
- Atomic 原子類的適用場景
- 計數器:如統計訪問量、任務完成數等。
- 狀態標志:如開關狀態、任務狀態等。
- 無鎖數據結構:如無鎖隊列、無鎖棧等。
- 高性能并發編程:在需要高性能的場景中替代鎖。
Atomic 原子類 通過 CAS 操作實現了無鎖的線程安全操作,適用于高性能并發場景。常用的 Atomic 原子類包括 AtomicInteger、AtomicLong、AtomicReference 等。在實際開發中,Atomic 原子類可以替代鎖,簡化并發編程并提升性能。
24、什么是CAS,存在什么問題
CAS(Compare-And-Swap) 是一種用于實現多線程同步的原子操作,它是無鎖編程的核心技術之一。CAS 操作通過硬件指令直接支持,能夠在不需要鎖的情況下實現線程安全。
- CAS 的原理
-
CAS 操作包含三個操作數:
- 內存位置(V):需要更新的變量。
- 預期值(A):變量當前的值。
- 新值(B):希望更新為的值。
-
CAS 的操作步驟如下:
- 比較內存位置 V 的值與預期值 A。
- 如果相等,則將內存位置 V 的值更新為新值 B。
- 如果不相等,則不進行任何操作。
-
CAS 操作的偽代碼如下:
boolean compareAndSwap(V, A, B) {if (V == A) {V = B;return true;}return false;
}
- CAS 的問題
- ABA 問題:在 CAS 操作過程中,如果變量的值從 A 變為 B,又變回 A,CAS 無法感知中間的變化。
- 線程 1 讀取變量值為 A。
- 線程 2 將變量值從 A 改為 B,然后又改回 A。
- 線程 1 執行 CAS 操作,發現值仍然是 A,認為沒有變化,但實際上變量已經被修改過。
解決方法:
使用版本號或時間戳標記變量的變化。
示例:Java 中的 AtomicStampedReference 和 AtomicMarkableReference。
- 循環時間長開銷大
- 如果 CAS 操作失敗,線程會不斷重試,導致 CPU 開銷增加。
- 在高競爭環境下,CAS 的性能可能不如鎖。
解決方法:
限制重試次數,或結合退避算法(如指數退避)。
- 只能保證一個變量的原子性
- CAS 只能保證單個變量的原子性,無法保證多個變量的原子性。
- 如果需要同時更新兩個變量,CAS 無法直接實現。
解決方法:
使用鎖或其他同步機制。
Java中CAS的使用
Java中大量使用的CAS,比如java.util.concurrent.atomic包下有很多的原子類AtomicInteger、AtomicBoolean…這些類提供對int、boolean等類型的原子操作,而底層就是通過CAS機制實現的。
比如AtomicInteger類有一個實例方法,叫做incrementAndGet,這個方法就是將AtomicInteger對象記錄的值+1并返回,與i++類似。但是這是一個原子操作,不會像i++一樣,存在線程不一致問題,因為i++不是原子操作。比如如下代碼,最終一定能夠保證num的值為200:
// 聲明一個AtomicInteger對象
AtomicInteger num = new AtomicInteger(0);
// 線程1
new Thread(() -> {for (int i = 0; i < 100; i++) {// num++num.incrementAndGet();}
}).start();
// 線程2
new Thread(() -> {for (int i = 0; i < 100; i++) {// num++num.incrementAndGet();}
}).start();Thread.sleep(1000);
System.out.println(num);
25、如何理解AQS
AQS就是AbstractQueuedSynchronizer抽象類,AQS其實就是JUC包下的一個基類,JUC下的很多內容都是基于AQS實現了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞隊列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS實現。
(1)首先AQS中提供了一個由volatile修飾,并且采用CAS方式修改的int類型的state變量。
public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {// 同步state成員變量,0表示無人占用鎖,大于1表示鎖被占用需要等待;private volatile int state;/*CLH隊列* <pre>* +------+ prev +-----+ +-----+* head | | <---- | | <---- | | tail* +------+ +-----+ +-----+* </pre>*/// 通過state自旋判斷是否阻塞,阻塞的線程放入隊列,尾部入隊,頭部出隊static final class Node{}private transient volatile Node head;private transient volatile Node tail;
}
(2)其次AQS中維護了一個雙向鏈表,有head,有tail,并且每個節點都是Node對象
static final class Node {// 表示線程以共享的模式等待鎖static final Node SHARED = new Node();// 表示線程以獨占的方式等待鎖static final Node EXCLUSIVE = null;//表示線程獲取鎖的請求已經取消了static final int CANCELLED = 1;//表示線程準備解鎖static final int SIGNAL = -1;// 表示節點在等待隊列紅,節點線等待喚醒static final int CONDITION = -2;// 表當前節點線程處于共享模式,鎖可以傳遞下去static final int PROPAGATE = -3;// 表示節點在隊列中的狀態volatile int waitStatus;volatile Node prev;// 前序指針volatile Node next;// 后序指針volatile Thread thread; // 當前節點的線程Node nextWaiter;// 指向下一個處于Condition狀態的節點final Node predecessor();//一個方法,返回前序節點prev,沒有的話拋出NPE(空指針異常)
}
AQS的核心原理
當多線程訪問共享資源(state)時,流程如下:
- 當線程1、2、3通過cas獲取state時,如果線程1獲取到了資源的使用權,令當前鎖的owenerThread設置為當前線程,state+1。
- 線程2、3未獲取到共享資源,將會被加入等待隊列(CLH 雙端鏈表隊列)。
- 當線程1釋放state時,令當前鎖的ownerThread設置為null,state-1。
- 這里要根據是否是公平鎖來競爭資源,如果是公平鎖,將會按照等待隊列的順序依次獲取資源。如果不是公平鎖,等待隊列中的第一個線程將會和新進來的線程競爭獲取資源。
AQS喚醒節點為何從后往前找
node節點在插入整個AQS隊列當中時是先把當前節點的上一個指針指向前面的節點,再把tail指向自己,這個時候會有一個CPU調度問題,如果這個時候我卡在這個位置,那么從前往后找就會造成節點丟失,就會出現找到空的節點的問題無法實現有效的線程喚醒導致出現死鎖的問題
aqs中的取消節點的方法,cancelAcquire也是先去調整上一個指針的指向,next指針后續才動,所以無論是我們節點插入的過程還是某一個節點取消個更改指針的過程,都是先動上一個指針再動next的,所以prex這個節點指向相對來說優先級更高或者時效性更好。
總結因為從前往后極大可能錯過某一個節點,從而造成某一個node在那邊被掛起了,但是你之前的線程已經釋放資源了并沒有被喚醒,造成鎖饑餓問題,總的來說AQS在喚醒節點時候從后往前找只要是為了找一個有效且可以被喚醒的接點,來保證并發程序的效率。
26、什么是 Java 內存模型
Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。
而JMM就作用于工作內存和主存之間數據同步過程。他規定了如何做數據同步以及什么時候做數據同步。
所以,再來總結下,JMM是一種規范,目的是解決由于多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。
Java內存模型的實現
在開發多線程的代碼的時候,我們可以直接使用synchronized
等關鍵字來控制并發,從來就不需要關心底層的編譯器優化、緩存一致性等問題。所以,Java內存模型,除了定義了一套規范,還提供了一系列原語,封裝了底層實現后,供開發者直接使用。
并發編程要解決原子性、有序性和一致性的問題,我們就再來看下,在Java中,分別使用什么方式來保證。
原子性
在Java中,為了保證原子性,提供了兩個高級的字節碼指令monitorenter
和monitorexit
,在Java中可以使用synchronized
來保證方法和代碼塊內的操作是原子性的。
可見性
Java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存作為傳遞媒介的方式來實現的。
Java中的volatile
關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile
來保證多線程操作時變量的可見性。
除了volatile
,Java中的synchronized
關鍵字也可以實現可見性。只不過實現方式不同。
有序性
在Java中,可以使用synchronized
和volatile
來保證多線程之間操作的有序性。實現方式有所區別:
volatile
關鍵字會禁止指令重排。synchronized
關鍵字保證同一時刻只允許一條線程操作。
27、三個線程分別順序打印 0-100
public class H {private static volatile int count = 0;public void yang() {Runnable a = () -> {while (count <= 100) {synchronized (this) {String s = Thread.currentThread().getName().split("-")[1];try {while (count % 3 != Integer.parseInt(s)) {this.wait();}if (count <= 100) {System.out.println(Thread.currentThread().getName() + ":" + count++);}this.notifyAll();} catch (Exception e) {e.printStackTrace();}}}};Thread thread0 = new Thread(a);Thread thread1 = new Thread(a);Thread thread2 = new Thread(a);thread0.start();thread1.start();thread2.start();}public static void main(String[] args) {H h = new H();h.yang();}
}
28、JMM(Java Memory Model)
JMM 是 Java 語言規范中定義的一種抽象的內存模型,它定義了多線程環境下,線程如何與主內存和工作內存交互,以及如何保證多線程程序的可見性、有序性和原子性。
JMM 關注的是多線程并發編程中的內存一致性問題,而不是內存的物理劃分。
JMM 的核心概念
- 主內存(Main Memory)
所有線程共享的內存區域,存儲共享變量(如實例變量、靜態變量)。
- 工作內存(Working Memory)
每個線程私有的內存區域,存儲線程對共享變量的副本。線程對變量的所有操作(讀/寫)都發生在工作內存中。
- 內存交互操作
JMM 解決的問題
(1)可見性(Visibility)
一個線程對共享變量的修改,其他線程是否能夠立即看到。通過 volatile 關鍵字、synchronized 鎖等機制保證可見性。
(2)有序性(Ordering)
程序執行的順序是否與代碼編寫的順序一致。通過 happens-before 規則和內存屏障(Memory Barrier)保證有序性。
(3)原子性(Atomicity)
一個操作是否是不可分割的。通過 synchronized 鎖、java.util.concurrent.atomic 包中的原子類等機制保證原子性。
JMM 的實際應用
(1)volatile 關鍵字
- 保證變量的可見性,禁止指令重排序。
- 適用于狀態標志、雙重檢查鎖定等場景。
(2)synchronized 關鍵字
- 保證代碼塊的原子性和可見性。
- 適用于臨界區資源的保護。
(3)final 關鍵字
- 保證變量的不可變性和線程安全性。
(4)java.util.concurrent 包
- 提供線程安全的集合類(如 ConcurrentHashMap)、鎖(如 ReentrantLock)和原子類(如 AtomicInteger)。