為什么局部變量是線程安全的?

前言
方法中的變量(即局部變量)是不存在數(shù)據(jù)競爭(Data Race)的,也是線程安全的。為了理解為什么,我們先來了一下方法是如何被執(zhí)行的,然后再分析局部變量的安全性,最后再介紹利用局部變量不會共享的特點而產(chǎn)生的解決并發(fā)問題的一些技術(shù)。
方法是如何被執(zhí)行的
int?a = 7;
int[] b = fibonacci(a);
int[] c = b;以上代碼轉(zhuǎn)換成CPU指令執(zhí)行,方法的調(diào)用過程示意圖如下:(圖來自參考[1])

當(dāng)調(diào)用fibonacci(a)時,CPU要先找到方法fibonacci()的地址(在CPU堆棧寄存器中),然后跳轉(zhuǎn)到這個地址去執(zhí)行代碼(藍(lán)色線),最后CPU執(zhí)行完方法,再返回原來調(diào)用方法的下一條語句(紅色線)。
CPU找調(diào)用方法的參數(shù)和返回地址,是通過堆棧寄存器。CPU支持一種線性結(jié)構(gòu),因為與方法調(diào)用有關(guān),所以也稱為調(diào)用棧。
再舉個例子,有三個方法A、B、C。方法A中調(diào)用方法B,方法B中調(diào)用方法C。那么將會構(gòu)建出如下調(diào)用棧。每個方法在調(diào)用棧里都有自己的獨立空間,稱為棧幀。每個棧幀都有對應(yīng)方法需要的參數(shù)和返回地址。當(dāng)調(diào)用新方法時,會創(chuàng)建新的棧幀,并壓入調(diào)用棧;當(dāng)方法返回時,對應(yīng)的棧幀就會被自動彈出。即,棧幀和方法同生共死。

三個方法生成的調(diào)用棧如上圖所示。
不同的編程語言雖定義方法雖各有所異,但是它們執(zhí)行方法的原理卻是一致的:都是依靠棧結(jié)構(gòu)解決。Java語言雖然是靠虛擬機(jī)解釋執(zhí)行,但是方法的調(diào)用也是利用棧結(jié)構(gòu)解決的。
局部變量的存放位置
局部變量是定義在方法內(nèi),作用域也是在方法內(nèi)部。當(dāng)方法運行結(jié)束后,局部變量也就失效了。那么我們可以得出,局部變量的存放位置應(yīng)該在調(diào)用棧中。事實上,局部變量就是存放到調(diào)用棧中的。

調(diào)用棧與線程
兩個線程可以同時用不同的參數(shù)調(diào)用相同的方法,那么調(diào)用棧和線程之間是什么關(guān)系呢?答案就是:每個線程都有自己獨立的調(diào)用棧。

所以,Java方法里面的局部變量是不存在并發(fā)問題的。每個線程都有自己獨立的調(diào)用棧,局部變量保存在各自的調(diào)用棧中,不會被共享,自然也就沒有并發(fā)問題。
利用不共享解決并發(fā)問題的技術(shù): 線程封閉
當(dāng)多線程訪問沒有同步的可變共享變量時就會出現(xiàn)并發(fā)問題,而解決方案之一便是使變量不共享。變量不會和其他變量共享,也就不會存在并發(fā)問題。僅在單線程里訪問數(shù)據(jù),不需要同步,我們稱之為線程封閉。當(dāng)某個對象封閉在一個線程中時,這種用法將自動實現(xiàn)線程安全性,即使被封閉的對象本身不是線程安全的。
采用線程封閉技術(shù)的案例非常多。例如一種常見的應(yīng)用便為JDBC的Connection對象。從數(shù)據(jù)庫連接池中獲取一個Connection對象,在JDBC規(guī)范中并沒有要求這個Connection一定是線程安全的。數(shù)據(jù)庫連接池通過線程封閉技術(shù),保證一個Connection對象一旦被一個線程獲取之后,在這個Connection對象返回之前,連接池不會將它分配給其他線程,從而保證了Connection對象不會有并發(fā)問題。
線程封閉技術(shù)的一個具體實現(xiàn)是我們上面提到的局部變量的使用(棧封閉),還有一種需要提一下,即ThreadLocal類。
ThreadLoacl類
維持線程封閉性一種更規(guī)范方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象相關(guān)聯(lián)起來。ThreadLocal提供了get()和set()等訪問接口,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get()總是返回由當(dāng)前執(zhí)行線程在調(diào)用set()時設(shè)置的最新值。
ThreadLocal對象通常用于防止對可變的單實例變量(Singleton)或全局變量進(jìn)行共享。
例如,在單線程應(yīng)用程序中可能會維持一個全局的數(shù)據(jù)庫連接,并在線程啟動時初始化這個連接對象,從而避免在調(diào)用每個方法時都要傳遞一個Connection對象。由于JDBC的連接對象不一定線程安全的,因此,當(dāng)多線程應(yīng)用程序在沒有協(xié)同的情況下使用全局變量時,就不是線程安全的。通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接。
如以下代碼所示,利用ThreadLocal來維持線程的封閉性:(代碼來自參考[2])
public?class?ConnectionDispenser?{
????static?String DB_URL = "jdbc:mysql://localhost/mydatabase";
????private?ThreadLocal connectionHolder
????????= new?ThreadLocal() {
????????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();
????}
} 當(dāng)某個頻繁執(zhí)行的操作需要一個臨時對象,例如一個緩沖區(qū),而同時又希望避免在每次執(zhí)行時都重新分配該臨時對象,就可以使用這項技術(shù)。例如,在Java 5.0之前,Integer.toString()方法使用ThreadLocal對象來保存一個12字節(jié)大小的緩沖區(qū),用于對結(jié)果進(jìn)行格式化,而不是使用共享的靜態(tài)緩沖區(qū)(需要使用加鎖機(jī)制)或者每次調(diào)用時都分配一個新的緩沖區(qū)。
小結(jié)
知道方法是如何調(diào)用的也就明白了局部變量為什么是線程安全的。方法調(diào)用會產(chǎn)生棧幀,局部變量會放在棧幀的工作內(nèi)存中,線程之間不共享,故不存在線程安全問題。后面我們介紹了基于不共享解決并發(fā)問題的線程封閉技術(shù),除了不共享這種思想可以解決并發(fā)問題,還有兩種:使用不可變變量和正確使用同步機(jī)制。
原文鏈接:cnblogs.com/myworld7/p/12264504.html
