[併發理論基礎] 11 | Java線程(下):局部變量

[併發理論基礎] 11 | Java線程(下):爲什麼局部變量是線程安全的?

一、方法是如何被執行的

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

以上代碼轉換成CPU指令執行,方法的調用過程示意圖如下:
在這裏插入圖片描述
當調用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來維持線程的封閉性:

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字節大小的緩衝區,用於對結果進行格式化,而不是使用共享的靜態緩衝區(需要使用加鎖機制)或者每次調用時都分配一個新的緩衝區。

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