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

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

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

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

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

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