【Java並發基礎】局部變量是線程安全的


前言

方法中的變量(即局部變量)是不存在數據競爭(Data Race)的,也是線程安全的。為了理解為什么,我們先來了一下方法是如何被執行的,然后再分析局部變量的安全性,最后再介紹利用局部變量不會共享的特點而產生的解決並發問題的一些技術。

方法是如何被執行的

int a = 7;
int[] b = fibonacci(a);
int[] c = b;

以上代碼轉換成CPU指令執行,方法的調用過程示意圖如下:(圖來自參考[1])

當調用fibonacci(a)時,CPU要先找到方法fibonacci()的地址(在CPU堆棧寄存器中),然后跳轉到這個地址去執行代碼(藍色線),最后CPU執行完方法,再返回原來調用方法的下一條語句(紅色線)。

CPU找調用方法的參數和返回地址,是通過堆棧寄存器。CPU支持一種線性結構,因為與方法調用有關,所以也稱為調用棧

再舉個例子,有三個方法A、B、C。方法A中調用方法B,方法B中調用方法C。那么將會構建出如下調用棧。每個方法在調用棧里都有自己的獨立空間,稱為棧幀。每個棧幀都有對應方法需要的參數和返回地址。當調用新方法時,會創建新的棧幀,並壓入調用棧;當方法返回時,對應的棧幀就會被自動彈出。即,棧幀和方法同生共死。

三個方法生成的調用棧如上圖所示。

不同的編程語言雖定義方法雖各有所異,但是它們執行方法的原理卻是一致的:都是依靠棧結構解決。Java語言雖然是靠虛擬機解釋執行,但是方法的調用也是利用棧結構解決的。

局部變量的存放位置

局部變量是定義在方法內,作用域也是在方法內部。當方法運行結束后,局部變量也就失效了。那么我們可以得出,局部變量的存放位置應該在調用棧中。事實上,局部變量就是存放到調用棧中的

調用棧與線程

兩個線程可以同時用不同的參數調用相同的方法,那么調用棧和線程之間是什么關系呢?答案就是:每個線程都有自己獨立的調用棧

所以,Java方法里面的局部變量是不存在並發問題的。每個線程都有自己獨立的調用棧,局部變量保存在各自的調用棧中,不會被共享,自然也就沒有並發問題。

利用不共享解決並發問題的技術: 線程封閉

當多線程訪問沒有同步的可變共享變量時就會出現並發問題,而解決方案之一便是使變量不共享。變量不會和其他變量共享,也就不會存在並發問題。僅在單線程里訪問數據,不需要同步,我們稱之為線程封閉。當某個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即使被封閉的對象本身不是線程安全的。

采用線程封閉技術的案例非常多。例如一種常見的應用便為JDBC的Connection對象。從數據庫連接池中獲取一個Connection對象,在JDBC規范中並沒有要求這個Connection一定是線程安全的。數據庫連接池通過線程封閉技術,保證一個Connection對象一旦被一個線程獲取之后,在這個Connection對象返回之前,連接池不會將它分配給其他線程,從而保證了Connection對象不會有並發問題。

線程封閉技術的一個具體實現是我們上面提到的局部變量的使用(棧封閉),還有一種需要提一下,即ThreadLocal類。

ThreadLoacl類

維持線程封閉性一種更規范方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象相關聯起來。ThreadLocal提供了get()set()等訪問接口,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get()總是返回由當前執行線程在調用set()時設置的最新值。

ThreadLocal對象通常用於防止對可變的單實例變量(Singleton)或全局變量進行共享
例如,在單線程應用程序中可能會維持一個全局的數據庫連接,並在線程啟動時初始化這個連接對象,從而避免在調用每個方法時都要傳遞一個Connection對象。由於JDBC的連接對象不一定線程安全的,因此,當多線程應用程序在沒有協同的情況下使用全局變量時,就不是線程安全的。通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬於自己的連接。

如以下代碼所示,利用ThreadLocal來維持線程的封閉性:(代碼來自參考[2])

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            try {
                return DriverManager.getConnection(DB_URL);
            } catch (SQLException e) {
                throw new RuntimeException("Unable to acquire Connection, e");
            }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

當某個頻繁執行的操作需要一個臨時對象,例如一個緩沖區,而同時又希望避免在每次執行時都重新分配該臨時對象,就可以使用這項技術。例如,在Java 5.0之前,Integer.toString()方法使用ThreadLocal對象來保存一個12字節大小的緩沖區,用於對結果進行格式化,而不是使用共享的靜態緩沖區(需要使用加鎖機制)或者每次調用時都分配一個新的緩沖區。

小結

知道方法是如何調用的也就明白了局部變量為什么是線程安全的。方法調用會產生棧幀,局部變量會放在棧幀的工作內存中,線程之間不共享,故不存在線程安全問題。后面我們介紹了基於不共享解決並發問題的線程封閉技術,除了不共享這種思想可以解決並發問題,還有兩種:使用不可變變量和正確使用同步機制。

參考:
[1]極客時間專欄王寶令《Java並發編程實戰》
[2]Brian Goetz.Tim Peierls. et al.Java並發編程實戰[M].北京:機械工業出版社,2016


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM