1. 程式人生 > 實用技巧 >被逼無奈的程式設計師,怒而整理多執行緒面試必問原始碼知識點

被逼無奈的程式設計師,怒而整理多執行緒面試必問原始碼知識點

本文是多執行緒系列之一,主要介紹多執行緒中比較基本的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掃碼獲取資料看這裡

https://gitee.com/biwangsheng/personal.git