面試專題之多線程

題目

  1. synchronized與lock的區別,使用場景。看過synchronized的源碼沒
  2. volatile關鍵字的如何保證內存可見性
  3. 你說你熟悉併發編程,那麼你說說Java鎖有哪些種類,以及區別
  4. 如何保證內存可見性
  5. Thread 類的 sleep()方法和對象的 wait()方法都可以讓線程暫停執行,它們有什麼區別?
  6. 線程的 sleep()方法和 yield()方法有什麼區別?
  7. 當一個線程進入一個對象的 synchronized 方法 A 之後,其它線程是否可進入此對象的 synchronized 方法 B?
  8. 請說出與線程同步以及線程調度相關的方法。
  9. 編寫多線程程序有幾種實現方式?
  10. synchronized 關鍵字的用法?
  11. 舉例說明同步和異步。
  12. 啓動一個線程是調用 run()還是 start()方法?
  13. 什麼是線程池(thread pool)?
  14. 線程的基本狀態以及狀態之間的關係?
  15. Java 中能創建 volatile 數組嗎?
  16. volatile 能使得一個非原子操作變成原子操作嗎?
  17. volatile 修飾符的有過什麼實踐?
  18. volatile 類型變量提供什麼保證?
  19. 10 個線程和 2 個線程的同步代碼,哪個更容易寫?
  20. 你是如何調用 wait()方法的?使用 if 塊還是循環?爲什麼?
  21. 什麼是多線程環境下的僞共享(false sharing)?
  22. 什麼是 Busy spin?我們爲什麼要使用它?
  23. Java 中怎麼獲取一份線程 dump 文件?
  24. Swing 是線程安全的?
  25. 什麼是線程局部變量?
  26. 用 wait-notify 寫一段代碼來解決生產者-消費者問題?
  27. 用 Java 寫一個線程安全的單例模式(Singleton)?

答案

1. synchronized與lock的區別,使用場景。看過synchronized的源碼沒
在這裏插入圖片描述
推薦閱讀Synchronized與Lock的區別與應用場景

2. volatile關鍵字的如何保證內存可見性
volatile修飾的變量保證其每個寫操作後都更新到主內存,每個讀操作都到主內存中更新,具體的話是在JVM層面,在修飾的變量前後加關鍵字
順帶一提volatile還能防止指令重排,這兩者的實現方式都是內存屏障。
3. 你說你熟悉併發編程,那麼你說說Java鎖有哪些種類,以及區別

公平鎖/非公平鎖
這個是在ReentrankLock中實現的,synchronized沒有,是用一個隊列實現的,在公平鎖好理解,就是先進這個隊列的,也先出隊列獲得資源,而非公平鎖的話,則是還沒有進隊列之前可以與隊列中的線程競爭嘗試獲得鎖,如果獲取失敗,則進隊列,此時也是要乖乖等前面出隊纔行

可重入鎖
如果一個線程獲得過該鎖,可以再次獲得,主要是用途就是在遞歸方面,還有就是防止死鎖,比如在一個同步方法塊中調用了另一個相同鎖對象的同步方法塊

獨享鎖/共享鎖
共享鎖可以由多個線程獲取使用,而獨享鎖只能由一個線程獲取。
對ReentrantReadWriteLock其讀鎖是共享鎖,其寫鎖是獨佔鎖
讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀,寫寫的過程是互斥的。其中獲得寫鎖的線程還能同時獲得讀鎖,然後通過釋放寫鎖來降級。讀鎖則不能升級

互斥鎖/讀寫鎖
上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
互斥鎖在Java中的具體實現就是ReentrantLock
讀寫鎖在Java中的具體實現就是ReadWriteLock

樂觀鎖/悲觀鎖
樂觀鎖就是樂觀的認爲不會發生衝突,用cas和版本號實現
悲觀鎖就是認爲一定會發生衝突,對操作上鎖

分段鎖
在1.7的concurrenthashmap中有分段鎖的實現,具體爲默認16個的segement數組,其中segement繼承自reentranklock,每個線程過來獲取一個鎖,然後操作這個鎖下連着的map。

偏向鎖/輕量級鎖/重量級鎖
在jdk1.6中做了第synchronized的優化,
偏向鎖指的是當前只有這個線程獲得,沒有發生爭搶,此時將方法頭的markword設置成0,然後每次過來都cas一下就好,不用重複的獲取鎖
輕量級鎖:在偏向鎖的基礎上,有線程來爭搶,此時膨脹爲輕量級鎖,多個線程獲取鎖時用cas自旋獲取,而不是阻塞狀態
重量級鎖:輕量級鎖自旋一定次數後,膨脹爲重量級鎖,其他線程阻塞,當獲取鎖線程釋放鎖後喚醒其他線程。(線程阻塞和喚醒比上下文切換的時間影響大的多,涉及到用戶態和內核態的切換)
自旋鎖:在沒有獲取鎖的時候,不掛起而是不斷輪詢鎖的狀態

4. 如何保證內存可見性
volatile 通過內存屏障,synchronized 通過修飾的程序段同一時間只能由同一線程運行,釋放鎖前會刷新到主內存

5. Thread 類的 sleep()方法和對象的 wait()方法都可以讓線程暫停執行,它們有什麼區別?
sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束後會自動恢復(線程回到就緒狀態)。
wait()是 Object 類的方法,調用對象的 wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的 notify()方法(或 notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。
補充:可能不少人對什麼是進程,什麼是線程還比較模糊,對於爲什麼需要多線程編程也不是特別理解。簡單的說:進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,是操作系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是 CPU 調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時通常擁有獨立的內存單元,而線程之間可以共享內存。使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程序對於其他程序是不友好的,因爲它可能佔用了更多的 CPU 資源。當然,也不是線程越多,程序的性能就越好,因爲線程之間的調度和切換也會浪費 CPU 時間。時下很時髦的 Node.js就採用了單線程異步 I/O 的工作模式。

6. 線程的 sleep()方法和 yield()方法有什麼區別?
(1) sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
(2) 線程執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;
(3)sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異常;
(4)sleep()方法比 yield()方法(跟操作系統 CPU 調度相關)具有更好的可移植性。
7. 當一個線程進入一個對象的 synchronized 方法 A 之後,其它線程是否可進入此對象的 synchronized 方法 B?
不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因爲非靜態方法上的 synchronized 修飾符要求執行方法時要獲得對象的鎖,如果已經進入A 方法說明對象鎖已經被取走,那麼試圖進入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖

8. 請說出與線程同步以及線程調度相關的方法。
(1) wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖;
(2)sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理 InterruptedException 異常
(3)notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由 JVM 確定喚醒哪個線程,而且與優先級無關;
(4)notityAll():喚醒所有處於等待狀態的線程,該方法並不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;
補充:Java 5 通過 Lock 接口提供了顯式的鎖機制(explicit lock),增強了靈活性以及對線程的協調。Lock 接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了 newCondition()方法來產生用於線程之間通信的 Condition 對象;此外,Java 5 還提供了信號量機制(semaphore),信號量可以用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問之前,線程必須得到信號量的許可(調用 Semaphore 對象的 acquire()方法);在完成對資源的訪問後,線程必須向信號量歸還許可(調用 Semaphore 對象的 release()方法)。

9. 編寫多線程程序有幾種實現方式?
Java 5 以前實現多線程有兩種實現方法:一種是繼承 Thread 類;另一種是實現Runnable 接口。兩種方式都要通過重寫 run()方法來定義線程的行爲,推薦使用後者,因爲 Java 中的繼承是單繼承,一個類有一個父類,如果繼承了 Thread 類就無法再繼承其他類了,顯然使用 Runnable 接口更爲靈活。
補充:Java 5 以後創建線程還有第三種方式:實現 Callable 接口,該接口中的 call方法可以在線程執行結束時產生一個返回值。

/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/29 11:16
 * @Description:創建線程的三種方式
 */
public class getthread {

    public static void main(String[] args) throws Exception {
      TestThread01 testThread01 = new TestThread01();
      testThread01.start();


      new Thread(() -> System.out.println("方式二:通過實現runable接口創建線程")).start();

        TestThread03 testThread03 = new TestThread03();
        Integer call = testThread03.call();
        System.out.println(call);
    }
}

class TestThread01 extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("方式一:通過繼承Thread類創建線程");
    }
}
class TestThread03 implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("方式三:jdk5以後通過實現callable接口創建線程");
        return 1000;
    }
}

10. synchronized 關鍵字的用法?
synchronized 關鍵字可以將對象或者方法標記爲同步,以實現對對象和方法的互斥訪問,可以用 synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將 synchronized 作爲方法的修飾符。

11. 舉例說明同步和異步。
如果系統中存在臨界資源(資源數量少於競爭資源的線程數量的資源),例如正在寫的數據以後可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那麼這些數據就必須進行同步存取(數據庫操作中的排他鎖就是最好的例子)。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而異步就是非阻塞式操作。

12. 啓動一個線程是調用 run()還是 start()方法?
啓動一個線程是調用 start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由 JVM 調度並執行,這並不意味着線程就會立即運行。run()方法是線程啓動後要進行回調(callback)的方法。

13. 什麼是線程池(thread pool)?
在面向對象編程中,創建和銷燬對象是很費時間的,因爲創建一個對象要獲取內存資源或者其它更多資源。在 Java 中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷燬後進行垃圾回收。所以提高服務程序效率的一個手段就是儘可能減少創建和銷燬對象的次數,特別是一些很耗資源的對象創建和銷燬,這就是”池化資源”技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。Java 5+中的 Executor 接口定義一個執行線程的工具。它的子類型即線程池接口是 ExecutorService。要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,因此在工具類 Executors 面提供了一些靜態工廠方法,生成一些常用的線程池,如下所示:
(1)newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
(2)newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
(3) newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60 秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說 JVM)能夠創建的最大線程大小。
(4)newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
(5)newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。
如果希望在服務器上使用線程池,強烈建議使用 newFixedThreadPool方法來創建線程池,這樣能獲得更好的性能。

14. 線程的基本狀態以及狀態之間的關係?

img

說明:其中 Running 表示運行狀態,Runnable 表示就緒狀態(萬事俱備,只欠CPU),Blocked 表示阻塞狀態,阻塞狀態又有多種情況,可能是因爲調用 wait()方法進入等待池,也可能是執行同步方法或同步代碼塊進入等鎖池,或者是調用了 sleep()方法或 join()方法等待休眠或其他線程結束,或是因爲發生了 I/O 中斷。

15. Java 中能創建 volatile 數組嗎?
能,Java 中可以創建 volatile 類型數組,不過只是一個指向數組的引用,而不是整個數組。我的意思是,如果改變引用指向的數組,將會受到 volatile 的保護,但是如果多個線程同時改變數組的元素,volatile 標示符就不能起到之前的保護作用了。

16. volatile 能使得一個非原子操作變成原子操作嗎?
一個典型的例子是在類中有一個 long 類型的成員變量。如果你知道該成員變量會被多個線程訪問,如計數器、價格等,你最好是將其設置爲 volatile。爲什麼?因爲 Java 中讀取 long 類型變量不是原子的,需要分成兩步,如果一個線程正在修改該 long 變量的值,另一個線程可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變量的讀寫是原子。

17. volatile 修飾符的有過什麼實踐?
一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是 64 位寬,因此對這兩種類型的讀是分爲兩部分的,第一次讀取第一個 32 位,然後再讀剩下的 32 位,這個過程不是原子的,但 Java 中volatile 型的 long 或 double 變量的讀寫是原子的。volatile 修復符的另一個作用是提供內存屏障(memory barrier),例如在分佈式框架中的應用。簡單的說,就是當你寫一個 volatile 變量之前,Java 內存模型會插入一個寫屏障(writebarrier),讀一個 volatile 變量之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有線程是可見的,因爲內存屏障會將其他所有寫的值更新到緩存。

18. volatile 類型變量提供什麼保證?
volatile 變量提供順序和可見性保證,例如,JVM 或者 JIT 爲了獲得更好的性能會對語句重排序,但是 volatile 類型變量即使在沒有同步塊的情況下賦值也不會與其他語句重排序。volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。某些情況下,volatile 還能提供原子性,如讀 64 位數據類型,像 long 和 double 都不是原子的,但 volatile 類型的 double 和long 就是原子的。

19. 10 個線程和 2 個線程的同步代碼,哪個更容易寫?
從寫代碼的角度來說,兩者的複雜度是相同的,因爲同步代碼與線程數量是相互獨立的。但是同步策略的選擇依賴於線程的數量,因爲越多的線程意味着更大的競爭,所以你需要利用同步技術,如鎖分離,這要求更復雜的代碼和專業知識。

20.你是如何調用 wait()方法的?使用 if 塊還是循環?爲什麼?
wait() 方法應該在循環調用,因爲當線程獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,循環檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:

// The standard idiom for using the wait methodsynchronized (obj) {
	while (condition does not hold)
	obj.wait();
	// (Releases lock, and reacquires on wakeup)
	... // Perform action appropriate to condition}

21. 什麼是多線程環境下的僞共享(false sharing)?
僞共享是多線程系統(每個處理器有自己的局部緩存)中一個衆所周知的性能問題。僞共享發生在不同處理器的上的線程對變量的修改依賴於相同的緩存行。

22. 什麼是 Busy spin?我們爲什麼要使用它?
Busy spin 是一種在不釋放 CPU 的基礎上等待事件的技術。它經常用於避免丟失 CPU 緩存中的數據(如果線程先暫停,之後在其他 CPU 上運行就會丟失)。所以,如果你的工作要求低延遲,並且你的線程目前沒有任何順序,這樣你就可以通過循環檢測隊列中的新消息來代替調用 sleep() 或 wait() 方法。它唯一的好處就是你只需等待很短的時間,如幾微秒或幾納秒。LMAX 分佈式框架是一個高性能線程間通信的庫,該庫有一個 BusySpinWaitStrategy 類就是基於這個概念實現的,使用 busy spin 循環 EventProcessors 等待屏障。

23. Java 中怎麼獲取一份線程 dump 文件?
在 Linux 下,你可以通過命令 kill -3 PID (Java 進程的進程 ID)來獲取 Java應用的 dump 文件。在 Windows 下,你可以按下 Ctrl + Break 來獲取。這樣 JVM 就會將線程的 dump 文件打印到標準輸出或錯誤文件中,它可能打印在控制檯或者日誌文件中,具體位置依賴應用的配置。如果你使用 Tomcat。

24. Swing 是線程安全的?
不是,Swing 不是線程安全的。你不能通過任何線程來更新 Swing 組件,如JTable、JList 或 JPanel,事實上,它們只能通過 GUI 或 AWT 線程來更新。這就是爲什麼 Swing供 invokeAndWait() 和 invokeLater() 方法來獲取其他線程的 GUI 更新請求。這些方法將更新請求放入 AWT 的線程隊列中,可以一直等待,也可以通過異步更新直接返回結果。你也可以在參考答案中查看和學習到更詳細的內容。

25. 什麼是線程局部變量?
線程局部變量是侷限於線程內部的變量,屬於線程自身所有,不在多個線程間共享。Java 提供 ThreadLocal 類來支持線程局部變量,是一種實現線程安全的方式。但是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別小心,在這種情況下,工作線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工作完成後沒有釋放,Java 應用就存在內存泄露的風險。

26. 用 wait-notify 寫一段代碼來解決生產者-消費者問題?
只要記住在同步塊中調用 wait() 和 notify()方 法 ,如果阻塞,通過循環來測試等待條件。

package com.edu.chapter03.test;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class Producer implements Runnable {
 
	private final Vector sharedQueue;
	private final int SIZE;
	
	public Producer(Vector sharedQueue, int size) {
		this.sharedQueue = sharedQueue;
		this.SIZE = size;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 7; i++) {
			System.out.println("Produced:" + i);
			try {
				produce(i);
			} catch (InterruptedException ex) {
				Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}
 
	private void produce(int i) throws InterruptedException {
		
		//wait if queue is full
		while (sharedQueue.size() == SIZE) {
			synchronized (sharedQueue) {
				System.out.println("Queue is full " + Thread.currentThread().getName()
						+ " is waiting , size: " + sharedQueue.size());
				sharedQueue.wait();
			}
		}
		
		//producing element and notify consumers
		synchronized (sharedQueue) {
			sharedQueue.add(i);
			sharedQueue.notifyAll();
		}
	}
}
package com.edu.chapter03.test;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class Consumer implements Runnable {
 
	private final Vector sharedQueue;
	private final int SIZE;
	
	public Consumer(Vector sharedQueue, int size) {
		this.sharedQueue = sharedQueue;
		this.SIZE = size;
	}
 
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (true) {
			try {
				System.out.println("Consumer: " + consume());
				Thread.sleep(50);
			} catch (InterruptedException ex) {
				Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}
	
	private int consume() throws InterruptedException {
		
		//wait if queue is empty
		while (sharedQueue.isEmpty()) {
			synchronized (sharedQueue) {
				System.out.println("Queue is empty " + Thread.currentThread().getName()
						+ " is waiting , size: " + sharedQueue.size());
				sharedQueue.wait();
			}
		}
		
		//otherwise consume element and notify waiting producer
		synchronized (sharedQueue) {
			sharedQueue.notifyAll();
			return (Integer) sharedQueue.remove(0);
		}
	}
}
package com.edu.chapter03.test;
import java.util.Vector;
 
public class ProducerConsumerSolution {
 
	public static void main(String[] args) {
		Vector sharedQueue = new Vector();
		int size = 4;
		Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");
		Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");
		prodThread.start();
		consThread.start();
	}
}

27. 用 Java 寫一個線程安全的單例模式(Singleton)?
單例的創建方式有7種:餓漢,懶漢(線程安全/不安全兩種),靜態內部類,枚舉和雙重檢驗(dcl模式),而當我們說線程安全時,意思是即使初始化是在多線程環境中,仍然能保證單個實例。下面幾種方法都是線程安全的,但枚舉是最好的實現方式(爲什麼請參考:深度分析 Java 的枚舉類型:枚舉的線程安全性及序列化問題

方法一:不使用同步方法:餓漢式(天生線程安全)

/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/2 11:39
 * @Description:餓漢式
 */
public class Test01Singleton {
    // 類初始化時,會立即加載該對象,線程天生安全,調用效率高
    private static Test01Singleton test01Singleton = new Test01Singleton();

    public Test01Singleton() {
        System.out.println("Test01Singleton類初始化");
    }

    public static Test01Singleton getInstance(){
        System.out.println("getInstance()");
        return test01Singleton;
    }

    public static void main(String[] args) {
        Test01Singleton instance = Test01Singleton.getInstance();
        Test01Singleton instance2 = Test01Singleton.getInstance();
        System.out.println(instance == instance2);
    }
}

缺點:不能延遲加載,只適合小項目

方法二:使用靜態內部類


/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/2 14:21
 * @Description:使用靜態內部類創建單例
 */
public class Test03Singleton {

    private Test03Singleton() {
        System.out.println("初始化類Test03Singleton");
    }

    public static class  Test03SingletonClassInstance {
         private static final Test03Singleton test03Singleton = new Test03Singleton();
    }

    public static Test03Singleton getInstance(){
        System.out.println("getInstance");
        return Test03SingletonClassInstance.test03Singleton;
     }

    public static void main(String[] args) {
        Test03Singleton s1 = Test03Singleton.getInstance();
        Test03Singleton s2 = Test03Singleton.getInstance();
        System.out.println(s1 == s2);
    }

}

方法三:使用同步方法

/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/2 11:55
 * @Description:懶漢式
 */
public class Test02Singleton {
    public static Test02Singleton test02Singleton;

    public Test02Singleton() {
        System.out.println("Test02Singleton類初始化。。。");
    }

    public synchronized  static Test02Singleton getInstace(){
        if (test02Singleton == null){
            test02Singleton =new Test02Singleton();
        }
        return test02Singleton;
    }

    public static void main(String[] args) {
        Test02Singleton test02Singleton = Test02Singleton.getInstace();
        Test02Singleton test02Singleton2 = Test02Singleton.getInstace();
        System.out.println(test02Singleton == test02Singleton2);
    }
}

方法四:使用雙重檢驗鎖

/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/2 15:05
 * @Description:使用雙重檢測鎖創建單例
 */
public class Test05Singleton {
    private static Test05Singleton test05Singleton;

    public Test05Singleton() {
        System.out.println("類初始化");
    }
    public static Test05Singleton getInstance(){
        if (test05Singleton == null){
            synchronized (Test05Singleton.class){
                if (test05Singleton == null){
                    test05Singleton = new Test05Singleton();
                }
            }
        }
        return test05Singleton;
    }

    public static void main(String[] args) {
        Test05Singleton instance = Test05Singleton.getInstance();
        Test05Singleton instance2 = Test05Singleton.getInstance();
        System.out.println(instance == instance2);
    }
}

方法五:枚舉(最好)

package designpattern.singleton;

/**
 * @Auther: 洺潤Star
 * @Date: 2020/3/2 14:47
 * @Description:使用枚舉來創建單例
 */
public class Test04Singleton {
    private static enum Test04SingletonEnum{
        INSTANCE;
        private Test04Singleton test04Singleton;

        Test04SingletonEnum() {
            System.out.println("初始化");
            test04Singleton = new Test04Singleton();
        }

        Test04Singleton getInstance(){
            return test04Singleton;
        }
    }

    public static Test04Singleton getInstace(){
        return Test04SingletonEnum.INSTANCE.getInstance();
    }
    public static void main(String[] args) {
        Test04Singleton u1 = Test04Singleton.getInstace();
        Test04Singleton u2 = Test04Singleton.getInstace();
        System.out.println(u1 == u2);
    }
}

當然關於多線程這一部分還有很多問題, 後續會更新面試-多線程(二)加入更多題目, 如果多線程知識有欠缺可查看本人的多線程專欄,不僅有知識總結還有系列教程推薦。

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