2020年大廠喜歡這樣問線程安全,這些知識點我整理好了

2020年,截止目前,我收到了阿里巴巴、騰訊、美團、京東、快手等互聯網大廠的面試邀請。求職是一場流程很長的拉鋸戰,涉及崗位選擇、簡歷投遞、簡歷評估、技術面試、HR面試等環節。

我發現在技術面試中多線程在面試中出現的次數非常非常多,幸好我面試之前也有所準備。今天結合面試經歷寫一篇面向面經的Java線程安全有關的最全知識彙總

本文很乾貨,很乾!請自帶茶水

進程和線程

進程 線程
量級 重量級 輕量級
內存 私有內存 共享內存
同步機制 不需要 需要
處理安全性 殺死進程是安全的 殺死線程是不安全的
  1. 進程:擁有整臺計算機的資源。私有空間,彼此隔離。
  • 多進程之間不共享內存
  • 進程之間通過消息傳遞進行協作
  • 一般來說,進程== 程序 ==應用,但一個應用中可能包含多個進程
  • OS支持的IPC機制(pipe/socket)支持進程間通信

不僅是本機的多個進程之間,也可以是不同機器的多個進程之間。

  • JVM通常運行單一進程,但也可以使用ProcessBuilder 創建新的進程。
  1. 線程
    程序內部的控制機制。
  • 進程=虛擬機;線程=虛擬CPU
  • 程序共享、資源共享,共享內存

線程Thread

從Thread類派生子類

public class HelloThread extends Thread {
	public void run() {
	System.out.println("Hello from a thread!");
	}
	public static void main(String args[]) {
	HelloThread p = new HelloThread();
	p.start();
	}
	//----------啓動該線程的兩個方式
	public static void main(String args[]) {
	(new HelloThread()).start();
	}
}

從Runnable接口構造Thread對象

public class HelloRunnable implements Runnable {
	public void run() {
	System.out.println("Hello from a thread!");
	}
	public static void main(String args[]) {
	(new Thread(new HelloRunnable())).start();
	}
}

常見創建方法:

new Thread(new Runnable() {
      @Override
      public void run() {
        
      }
    });

併發很難測試和調試因爲競爭條件導致的bug。因爲交錯interleaving的存在,導致很難復現bug

Thread.sleep():使得線程在一定時間內休眠,進入休眠的線程不會失去對現有monitor或鎖的所有權。

Thread.interrupt() 中斷
在這裏插入圖片描述
Thread.yield():使用該方法,線程告知調度器:我可以放棄CPU的佔用權,從而可能引起調度器喚醒其他線程(儘量避免在代碼中使用)。

public void run() {
...
	for (int i = 0; i < 5; i++) {
	if ((i % 5) == 0)
	Thread.yield();
	}
}

Thread.join():讓當前線程保持執行,直到其執行結束。

在這裏插入圖片描述

線程池

參考鏈接:https://www.cnblogs.com/cdf-opensource-007/p/8769777.html

Excutors創建線程池便捷方法如下:

Executors.newFixedThreadPool(100);//創建固定大小的線程池
Executors.newSingleThreadExecutor();//創建只有一個線程的線程池
Executors.newCachedThreadPool();//創建一個不限線程數上限的線程池,任何提交的任務都將立即執行

對於服務端需要長期運行的程序,創建線程池應該使用ThreadPoolExecutor的構造方法

public ThreadPoolExecutor(
      int corePoolPoolSize,//線程池長期維持的線程數
      int maximumPoolSize, //線程數的上限
      long keepAliveTime,//空閒線程存活時間
      TimeUnit unit,//時間單位
      BlockingQueue<Runnable> workQueue,//任務的排隊隊列
      ThreadFactory threadFactory,//新線程的產生方式
      RejectedExecutionHandler handler//拒絕策略
  )

java線程池有7大參數,四大特性。

特性一:當池中正在運行的線程數(包括空閒線程)小於corePoolSize時,新建線程執行任務。

特性二:當池中正在運行的線程數大於等於corePoolSize時,新插入的任務進入workQueue排隊(如果workQueue長度允許),等待空閒線程來執行。

特性三:當隊列裏的任務數達到上限,並且池中正在運行的線程數小於maximumPoolSize,對於新加入的任務,新建線程。

特性四:當隊列裏的任務數達到上限,並且池中正在運行的線程數等於maximumPoolSize,對於新加入的任務,執行拒絕策略(線程池默認的拒絕策略是拋異常)。

線程安全的策略

  1. 限制數據共享

  2. 共享不可變數據

  3. 共享線程安全的可變數據

  4. 同步機制:通過鎖的機制共享線程不安全的可變數據,變並行爲串行

非同步機制

策略一:限制數據共享

線程之間不共享mutable數據類型

import java.math.BigInteger;
public class Main {
    public static void computeFact(final int n){
        BigInteger result = new BigInteger("1");
        for(int i = 1;i<=n;i++) {
            System.out.printf("Fact %d is working\n",i);
            result = result.multiply(new BigInteger(String.valueOf(i)));
        }
        System.out.printf("Fact %d is %d\n",n,result);
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                computeFact(99);
            }
        });
        thread1.start();
        computeFact(100);
    }
}

避免全局變量。例如下面是不安全的,存在多個線程,同時訪問getInstance()方法,創建出兩個PinballSimulator 對象。

在這裏插入圖片描述

策略二: Immutability

使用不可變數據類型和不可變引用,避免多線程之間的race condition

策略三:線程安全的數據類型

如果必須要用mutable的數據類型在多線程之間共享數據,要使用線程安全的數據類型。

同步機制

Lock鎖機制

可重入鎖:基於線程的分配。

讀寫鎖:對一個資源的訪問分成了2個鎖,比如文件,分爲讀鎖和寫鎖。例如ReadWriteLock()

可中斷鎖:可以中斷的鎖機制。例如Lock是可中斷鎖。

公平鎖: 以請求鎖的順序來獲取鎖。有多個線程在等待一個鎖,當鎖被釋放時,等待時間最久的線程會獲取該鎖,公平鎖。

syncronized

代碼塊

對某一代碼塊使用,sychronized後面的括號裏面是變量,一次只有一個線程進入該代碼塊

public void synchroMethod(int m){
        synchronized (m){
            
        }
    }

方法聲明時

方法聲明時使用,表示一次只能有一個線程進入該方法,其他線程要想在此時調用該方法,只能排隊。

public synchronized void synchroMethod(int m){
        
}

synchronized後面括號裏是對象

線程獲得的是對象鎖,synchronized後面括號裏是一個對象。

public  void synchroMethod(int m){
        synchronized(this){
            
        }
    }

注意,構造方法沒有必要使用synchronized方法,因爲構造方法的對象在從構造器返回之前一直被限制在單線程中。

synchronized method 與 synchronized(this) block的區別:

  • 後者需要顯式的給出lock,且不一定非要是this

  • 後者可停工更細粒度的併發控制

同步機制給性能帶來極大影響。除非必要,否則不要用。Java中很多mutable的類型都不是threadsafe就是這個原因。

儘可能減小lock的範圍。直接使用synchronized同步Method,說明沒有先思考清楚到底lock誰,然後再synchronized(…)。

public static synchronized boolean findReplace(EditBuffer buf, ...)

將獲得靜態鎖,在class層面上鎖。同一時間內只有一個線程能夠執行該方法,即使其他線程在不同的內存區取用數據,是安全的。這對性能帶來極大損耗。

  • Synchronized不是靈丹妙藥,你的程序需要嚴格遵守設計原則,先嚐試其他辦法,實在做不到再考慮lock。

  • 所有關於threadsafe的設計決策也都要在ADT中記錄下來。

如果A線程在synchronized (list) { … }

ArraysList方法的add(),其中size是全局變量,沒有synchronization,存在線程不安全的風險

public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

Collections.synchronizedList方法是線程安全的。

List<String> sharedList = Collections.synchronizedList(new ArrayList<String>());

在iterate部分需要使用synchronized(sharedList) { … } 來block

  • synchronized static是某個類的範圍,它可以對類的所有對象實例起作用。

  • synchronized 是某實例的範圍,只對一個實例起作用,synchronized isSync(){}防止多個線程同時訪問這個實例中的synchronized 方法。

Locking原則

  • 任何共享的mutable變量/對象必須被lock所保護

  • 如果一個不變量涉及到多個mutable變量的時候,它們必須被同一個lock所保護

  • monitor pattern中,ADT所有方法都被同一個synchronized(this)所保護

原子操作

不可被中斷的一個或一系列操作。減少內存一致性的錯誤風險。

  • 輕量級的同步機制

  • 原子變量的改變對於其他線程是可見的

private volatile int counter;

優點:比synchronized更加有效

缺點:需要更多地關注內存一致性

在這裏插入圖片描述

Liveness: deadlock, starvation and livelock

死鎖

多個線程競爭lock,相互等待對方釋放lock

在這裏插入圖片描述
在這裏插入圖片描述

死鎖的解決方案

  1. 設置鎖獲取的順序

在這裏插入圖片描述
缺點:

  • 它不是模塊化的-代碼必須知道系統或至少子系統中的所有鎖。

  • 在獲取第一個鎖之前,代碼可能很難知道它將需要哪些鎖。它可能需要做一些計算才能弄清楚。

在這裏插入圖片描述
2. coarser locking
使用單個鎖監管多個對象實例,甚至是程序的一個子系統。

例如,下列代碼使用Castle的對象鎖進行同步化
在這裏插入圖片描述
缺點:

  • 用單個鎖監聽很多可變數據,不能實時獲取這些數據

  • 在最壞的情況下,用一個鎖保護程序中所有的東西,程序變成單線程

Starvation

因爲其他線程lock時間太長,一個線程長時間無法獲取其所需的資源訪問權(lock),導致無法往下進行。

Livelock

一個線程經常會響應另一個線程的動作。

線程不會被阻斷,他們可能忙於響應其他線程而不能恢復工作。
在這裏插入圖片描述

線程間協作的方法

wait()

該操作使object所處的當前線程進入阻塞/等待狀態,直到其他線程調用該對象的notify()操作

public synchronized void guardedJoy() {
// This guard only loops once for each special event,
// which may not be the event we're waiting for.
while(!joy) {
try {
	wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}

notify()與notifyAll()

隨機選擇一個在該對象上調用wait方法的線程,解除其阻塞狀態

public synchronized notifyJoy() {
	joy = true;
	notifyAll();
}

notify()與notifyAll()的區別

總結

咱們玩歸玩,鬧歸鬧,別拿面試開玩笑。

線程安全在面試中出現的次數非常非常多,一旦問到了,大家一定要回答全面,不要丟三落四,回答到點上。大家面試前要把基礎打牢,多寫併發線程的程序代碼,多線程在筆試題中也很常見。

如果有收穫?希望老鐵們來個三連,點贊、收藏、轉發

創作不易,別忘點個贊,可以讓更多的人看到這篇文章,順便鼓勵我寫出更好的博客

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