被逼無奈的程式設計師,怒而整理多執行緒面試必問原始碼知識點
本文是多執行緒系列之一,主要介紹多執行緒中比較基本的synchronized和volatile。
起因
很簡單,別逼無奈,天知道這群大佬怎麼想的,用什麼思考的面試題,你面試阿里這一類程式設計航母也就罷了,問題是一些中型企業,在面試的時候也問的相當底層,剛開始我沒在意,後來面試了幾家公司這一塊回答的模模糊糊,然後面完了就沒有下文了,太#@###...&&&***,你有什麼辦法?沒辦法,整唄,幸好還有點時間
這裡就面試中我最常見到的兩個問題給大家解答一下
一、synchronized
1.1 synchronized使用
synchronized是java的關鍵字,用來對資源加鎖,在多執行緒的環境下,不可避免的要用到共享資源,此時可以使用synchronized關鍵字對共享資源解鎖,防止多個執行緒同時對共享資源操作。
synchronized的使用相對簡單,以下面的程式碼為例,想要訪問synchronized修飾的程式碼塊,必須先獲得物件o的鎖。這裡鎖定的就是物件o。
public class ThreadTest {
private int count = 10;
Object o = new Object();
private void m1(){
synchronized (o){
count --;
System.out.println(Thread.currentThread().getId()+":count="+count);
}
}
public static void main(String[] args){
ThreadTest tt = new ThreadTest();
new Thread(()->tt.m1(), "t1").start();
new Thread(()->tt.m1(), "t2").start();
}
}
實際上並不用每次都要new一個無用的物件,我們可以使用this物件,鎖定當前物件就可以。
synchronized (this){
count --;
System.out.println(Thread.currentThread().getId()+":count="+count);
}
synchronized還可修飾方法,在修飾方法的時候與該方法的一開始鎖定this物件是一樣的,也就是說下面兩種寫法是一樣的。
private void m1(){
synchronized (this){
count --;
System.out.println(Thread.currentThread().getId()+":count="+count);
}
}
private synchronized void m1(){
count --;
System.out.println(Thread.currentThread().getId()+":count="+count);
}
如果是靜態方法,synchronized (this)鎖定的就是類本身。
synchronized的使用相對簡單,我們在使用synchronized進行加鎖時,如果一個方法只有很小的一部分需要解鎖,那就不要給整個方法加鎖,但是如果一段程式碼中有好幾個部分需要加鎖,那就不要加多個鎖,可以在整個方法上加鎖,這樣避免了所得頻繁建立。
1.2 synchronized原理
使用synchronized加鎖並不會剛一開始就向cpu申請鎖定資源,而是經歷了一個鎖升級的過程。假如第一個執行緒來訪問共享資源,剛開始的時候只是在物件的頭記錄了執行緒id,現在只是一個偏向鎖,如果此時又來也一個執行緒需要申請鎖,鎖就會升級成自旋鎖,就是執行緒會原地等待,通過一個while true的迴圈,迴圈十次,直到鎖釋放,如果迴圈十次還沒有被釋放,才會向作業系統申請資源,這種情況就變成了重量級鎖。
synchronized是可重入的,如果m1和m2都用synchronized修飾,而且鎖定的都是同一個物件,此時如果m1呼叫m2,同一個執行緒已經獲取了這個物件的鎖,那麼m1呼叫m2是不需要再申請鎖的。
二、Volatile
Volatile是java的關鍵字,用來修飾變數,它有兩個作用:
1. 禁止指令重新排序
我們的程式碼在被jvm編譯成位元組碼時,jvm會對我們的程式碼的執行順序進行優化,當然這個優化保證不會改變執行的結果,在多執行緒的情況下,指令重排序有可能會導致執行緒安全問題,而且這個問題很難發現,所以我們用Volatile,禁止指令重新排序。
2. 使一個變數在多個執行緒中可見。
大家都知道java是有堆記憶體的,堆記憶體是所有執行緒的共享記憶體,但是每個執行緒又有自己的私有記憶體空間,假如有執行緒A 和執行緒B,兩個執行緒都要訪問變數t,執行緒A 和 B都會將tcopy一份到自己的執行緒空間中,這樣他們對t所做的修改彼此是不知道的,因為執行緒並不會將t立刻寫回堆記憶體,同樣,執行緒也不會每次都從堆記憶體將t重新copy。如果加了Volatile修飾,如果執行緒對t做了修改,就會強制將t立刻寫回堆記憶體,同樣的,當執行緒使用t時,也會強制將t從堆記憶體重新copy。這樣就保證了執行緒對變數的修改對其他執行緒是可見的。
但是Volatile並不能代替synchronized,如果多個執行緒同時修改t,只加了Volatile限制,同樣會出問題。例如下面這段程式碼:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ThreadTest1{
private volatile int count = 10000;
private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
public void run(){
count --;
System.out.println(Thread.currentThread().getName()+":count="+count);
}
public static void main(String[] args){
ThreadTest1 tt1 = new ThreadTest1();
Integer limit = 10000;
for(int i=0; i<limit; i++){
new Thread(tt1::run, "Thread"+i).start();
}
}
}
執行這段程式碼就會發現,count並不會按順序減少,而是有重複出現的情況。所以Volatile並不能代替Synchronized。
關於Synchronized和Volatile就介紹到這裡,關於多執行緒,其實需要了解的東西還是很多的,不信?往下看
我把遇到的問題整理形成了一張知識導圖
面試題
這是我在面試的過程中遇到的面試題,有回答上來的,也有沒回答上來的,面試完了後再網上查答案進行了相應的總結
參考文件
這是我在整理面試答案的過程中,無意發現的一份文件,整理的很詳細,都是從原始碼對於多執行緒進行講解,並且在每一章節的後面,都會有相應的知識圖譜進行知識點總結
相應的文章已經整理形成文件,git掃碼獲取資料看這裡