Java併發程式設計 | 區域性變數為什麼是執行緒安全的

語言: CN / TW / HK

theme: scrolls-light highlight: a11y-dark


持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第1天,點選檢視活動詳情

本系列專欄 Java併發程式設計專欄 - 元浩875的專欄 - 掘金 (juejin.cn)

前言

本篇文章說一個比較簡單的東西,就是區域性變數為什麼是執行緒安全的,對於熟悉JVM的開發者來說這個問題可以忽略不計。所以本篇文章作為知識回顧和擴充套件。

正文

本系列文章一直說的都是併發程式設計,即多個執行緒同時訪問共享變數的時候,會導致併發問題,那麼在Java語言裡,是不是所有變數都會是共享變數呢

區域性變數

比如我們看下面這個fibonacci()方法,會根據傳入的引數n,返回1到n的斐波那契數列,斐波那契數列數列類似這樣:1、1、2、3、5、8...,即第一項和第二項是1,第三項開始,每一項等於前倆項之和,所以方法如下:

kotlin int[] fibonacci(int n) { // 建立結果陣列 int[] r = new int[n]; // 初始化第一、第二個數 r[0] = r[1] = 1; // ① // 計算2..n for(int i = 2; i < n; i++) { r[i] = r[i-2] + r[i-1]; } return r; } 假如這時有多個執行緒來呼叫fibonacci()這個方法時,陣列r是否存在資料競爭呢

很多人都知道這裡的r是方法的區域性變數,是不存在資料競爭的,至於原因,我們就來梳理一下。

方法是如何被執行

想搞明白這個問題,必須瞭解一些編譯原理的知識或者JVM的知識。在CPU層面,是沒有方法概念的,在CPU眼裡,只有一條條的指令。編譯程式,負責把高階語言裡的方法轉換為一條條指令,所以這時可以站在編譯器實現者的角度來思考:怎麼完成方法到指令的轉換。

我們先來看一下下面的3行程式碼:

kotlin int a = 7; int[] b = fibonacci(a); int[] c = b; 先宣告一個變數a,然後呼叫fibonacci()方法,將返回值賦值給c;

這3句程式碼非常簡單,當呼叫fibonacci(a)的時候,CPU要先找到方法fibonacci()的地址,然後跳轉到這個地址上去執行程式碼,最後CPU執行完方法fibonacci()之後,要能夠返回。首先找到呼叫方法的下一條語句的地址:也就是int[] c=b;的地址,再跳轉到這個地址去執行。

這個呼叫過程,下圖可以加深理解:

image.png

到這裡,方法呼叫的過程基本就清楚了,但是還有一個很重要的問題,即CPU去哪裡尋找呼叫方法的引數和返回地址 這時肯定立即會想到:通過CPU的堆疊暫存器,CPU支援一種棧結構,因為這個棧是和方法呼叫相關的,因此經常被稱為呼叫棧。

假如有3個方法A、B、C,它們呼叫的關係是A -> B -> C,在執行時,就會構建出下面的呼叫棧。每個方法在呼叫棧裡都有自己的獨立空間,叫做棧幀,每個棧幀裡都有對應方法需要的引數和返回地址。

當呼叫方法時,會建立新的棧幀,並壓入呼叫棧;當方法返回時,對應的棧幀就會自動彈出,也就是說棧幀和方法是同生共死的。

image.png

利用棧結構來支援方法呼叫這個方案非常普遍,以至於CPU裡內建了棧暫存器。這裡雖然各種程式語言定義的方法千奇百怪,但是方法的內部執行原理幾乎都是一致的:都是靠棧結構來解決。

區域性變數存在哪裡

我們已經知道了方法在CPU眼裡是怎麼執行的,那方法的區域性變數儲存在哪裡?

首先我們得知道,區域性變數的作用域是方法內部,也就是說方法執行完,區域性變數就沒用了,即區域性變數和方法同生共死。這時就應該聯想到和方法同生共死的棧幀,所以把區域性變數儲存在棧幀中非常合理。

實時上,也是如此,區域性變數就是放在了呼叫棧中,如下圖:

image.png

在學習Java時,我們都知道new出來的物件在隊裡,區域性變數在棧中,只不過很多人不清楚是為什麼 其實就可以從變數的生命週期來思考,區域性變數和方法同生共死的,一個變數如果想跨越方法的邊界,就必須建立在堆中。

呼叫棧和執行緒

2個執行緒可以同時用不同的引數呼叫相同的方法,那呼叫棧和執行緒之間是什麼關係呢 答案也呼之欲出:每個執行緒都有自己獨立的呼叫棧。

因為如果不是這樣,那2個執行緒就互相干擾了,如下圖所示,執行緒A B C都有自己獨立的呼叫棧:

image.png

現在再來看最開始的問題,Java方法裡面的區域性變數是否存在併發問題 答案就是沒有併發問題,每個執行緒都有自己的呼叫棧,區域性變數儲存線上程各自的呼叫棧中,不會共享,也就沒有併發問題。

執行緒封閉

前面從區域性變數我們可以知道:沒有共享,就沒有傷害。這個思路就成為了解決併發程式設計的一個重要技術,叫做執行緒封閉,即僅在單執行緒內訪問資料,由於不存在共享,所以便不會出現併發問題。

總結

本篇文章解釋了局部變數為什麼是執行緒安全的,究其原因就是區域性變數沒有共享,而在探究這個過程中,我們做個總結: 1. 方法的執行以及拿到結果,是通過找到方法的位置,執行完拿到執行結果。 2. 在CPU眼中,是沒有方法的概念的,有的只是一個個的指令。 3. 每呼叫一次方法,就會建立一個棧幀,棧幀包括區域性變數和引數,其生命週期和方法是同生共死的。 4. 每個執行緒都有其獨立的呼叫棧,呼叫棧中是棧幀,所以區域性變數不會被共享。

歡迎大家點贊、收藏、評論,一起進步。