摘要:將與當(dāng)前線程建立一對(duì)一關(guān)系的值移除。為了讓方法里的操作具有原子性,也就是在一個(gè)線程執(zhí)行這一系列操作的同時(shí)禁止其他線程執(zhí)行這些操作,提出了鎖的概念。
上頭一直在說(shuō)以線程為基礎(chǔ)的并發(fā)編程的好處了,什么提高處理器利用率啦,簡(jiǎn)化編程模型啦。但是磚家們還是認(rèn)為并發(fā)編程是程序開(kāi)發(fā)中最不可捉摸、最詭異、最扯犢子、最麻煩、最惡心、最心煩、最容易出錯(cuò)、最不符合社會(huì)主義核心價(jià)值觀的一個(gè)部分~ 造成這么多最的原因其實(shí)很簡(jiǎn)單:進(jìn)程中的各種資源,比如內(nèi)存和I/O,在代碼里以變量的形式展現(xiàn),而某些變量在多線程間是共享、可變的,共享意味著這個(gè)變量可以被多個(gè)線程同時(shí)訪問(wèn),可變意味著變量的值可能被訪問(wèn)它的線程修改。圍繞這些共享、可變的變量形成了并發(fā)編程的三大殺手:安全性、活躍性、性能,下邊我們來(lái)詳細(xì)嘮叨這些風(fēng)險(xiǎn)~
共享變量的含義并不是所有內(nèi)存變量都可以被多個(gè)線程共享,在一個(gè)線程調(diào)用一個(gè)方法的時(shí)候,會(huì)在棧內(nèi)存上為局部變量以及方法參數(shù)申請(qǐng)一些內(nèi)存,在方法調(diào)用結(jié)束的時(shí)候,這些內(nèi)存便被釋放。不同線程調(diào)用同一個(gè)方法都會(huì)為局部變量和方法參數(shù)拷貝一個(gè)副本(如果你忘了,需要重新學(xué)習(xí)一下方法的調(diào)用過(guò)程),所以這個(gè)棧內(nèi)存是線程私有的,也就是說(shuō)局部變量和方法參數(shù)是不可以共享的。但是對(duì)象或者數(shù)組是在堆內(nèi)存上創(chuàng)建的,堆內(nèi)存是所有線程都可以訪問(wèn)的,所以包括成員變量、靜態(tài)變量和數(shù)組元素是可共享的,我們之后討論的就是這些可以被共享的變量對(duì)并發(fā)編程造成的風(fēng)險(xiǎn)~ 如果不強(qiáng)調(diào)的話,我們下邊所說(shuō)的變量都代表成員變量、靜態(tài)變量或者數(shù)組元素。
安全性原子性操作、內(nèi)存可見(jiàn)性和指令重排序是構(gòu)成線程安全性的三個(gè)主題,下邊我們?cè)敿?xì)看哈~
原子性操作我們先拿一個(gè)例子開(kāi)場(chǎng):
public class Increment { private int i; public void increase() { i++; } public int getI() { return i; } public static void test(int threadNum, int loopTimes) { Increment increment = new Increment(); Thread[] threads = new Thread[threadNum]; for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < loopTimes; i++) { increment.increase(); } } }); threads[i] = t; t.start(); } for (Thread t : threads) { //main線程等待其他線程都執(zhí)行完成 try { t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(threadNum + "個(gè)線程,循環(huán)" + loopTimes + "次結(jié)果:" + increment.getI()); } public static void main(String[] args) { test(20, 1); test(20, 10); test(20, 100); test(20, 1000); test(20, 10000); test(20, 100000); } }
其中,increase方法的作用是給成員變量i增1,test方法接受兩個(gè)參數(shù),一個(gè)是線程的數(shù)量,一個(gè)是循環(huán)的次數(shù),每個(gè)線程中都有一個(gè)將成員變量i增1給定循環(huán)次數(shù)的任務(wù),在所有線程的任務(wù)都完成之后,輸出成員變量i的值,如果沒(méi)有什么問(wèn)題的話,程序執(zhí)行完成后成員變量i的值都是threadNum*loopTimes。大家看一下執(zhí)行結(jié)果:
20個(gè)線程,循環(huán)1次結(jié)果:20 20個(gè)線程,循環(huán)10次結(jié)果:200 20個(gè)線程,循環(huán)100次結(jié)果:2000 20個(gè)線程,循環(huán)1000次結(jié)果:19926 20個(gè)線程,循環(huán)10000次結(jié)果:119903 20個(gè)線程,循環(huán)100000次結(jié)果:1864988
咦,貌似有點(diǎn)兒不對(duì)勁唉~再次執(zhí)行一遍的結(jié)果:
20個(gè)線程,循環(huán)1次結(jié)果:20 20個(gè)線程,循環(huán)10次結(jié)果:200 20個(gè)線程,循環(huán)100次結(jié)果:2000 20個(gè)線程,循環(huán)1000次結(jié)果:19502 20個(gè)線程,循環(huán)10000次結(jié)果:100157 20個(gè)線程,循環(huán)100000次結(jié)果:1833170
這就更令人奇怪了~~ 當(dāng)循環(huán)次數(shù)增加時(shí),執(zhí)行結(jié)果與我們預(yù)期不一致,而且每次執(zhí)行貌似都是不一樣的結(jié)果,這個(gè)是個(gè)什么鬼?
答:這個(gè)就是多線程的非原子性操作導(dǎo)致的一個(gè)不確定結(jié)果。
啥叫個(gè)原子性操作呢?就是一個(gè)或某幾個(gè)操作只能在一個(gè)線程執(zhí)行完之后,另一個(gè)線程才能開(kāi)始執(zhí)行該操作,也就是說(shuō)這些操作是不可分割的,線程不能在這些操作上交替執(zhí)行。java中自帶了一些原子性操作,比如給一個(gè)非long、double基本數(shù)據(jù)類型變量或者引用的賦值或者讀取操作。
為什么強(qiáng)調(diào)非long、double類型的變量?我們稍后看哈~
那i++這個(gè)操作不是一個(gè)原子性操作么?
答:還真不是,這個(gè)操作其實(shí)相當(dāng)于執(zhí)行了i = i + 1,也就是三個(gè)原子性操作:
讀取變量i的值
將變量i的值加1
將結(jié)果寫入i變量中
由于線程是基于處理器分配的時(shí)間片執(zhí)行的,在這個(gè)過(guò)程中,這三個(gè)步驟可能讓多個(gè)線程交叉執(zhí)行,為簡(jiǎn)化過(guò)程,我們以兩個(gè)線程交叉執(zhí)行為例,看下圖:
這個(gè)圖的意思就是:
線程1執(zhí)行increase方法先讀取變量i的值,發(fā)現(xiàn)是5,此時(shí)切換到線程2執(zhí)行increase方法讀取變量i的值,發(fā)現(xiàn)也是5。
線程1執(zhí)行將變量i的值加1的操作,得到結(jié)果是6,線程二也執(zhí)行這個(gè)操作。
線程1將結(jié)果賦值給變量i,線程2也將結(jié)果賦值給變量i。
在這兩個(gè)線程都執(zhí)行了一次increase方法之后,最后的結(jié)果竟然是變量i從5變到了6,而不是我們想象中的7。。。
另外,由于CPU的速度非常快,這種交叉執(zhí)行在執(zhí)行次數(shù)較低的時(shí)候體現(xiàn)的并不明顯,但是在執(zhí)行次數(shù)多的時(shí)候就十分明顯了,從我們上邊測(cè)試的結(jié)果上就能看出。
在真實(shí)編程環(huán)境中,我們往往需要某些涉及共享、可變變量的一系列操作具有原子性,我們可以從下邊三個(gè)角度來(lái)保證這些操作具有原子性。
從共享性解決如果一個(gè)變量變得不可以被多線程共享,不就可以隨便訪問(wèn)了唄哈哈,大致有下面這么兩種改進(jìn)方案。
盡量使用局部變量解決問(wèn)題
因?yàn)榉椒ㄖ械木植孔兞?包括方法參數(shù)和方法體中創(chuàng)建的變量)是線程私有的,所以無(wú)論多少線程調(diào)用某個(gè)不涉及共享變量的方法都是安全的。所以如果能將問(wèn)題轉(zhuǎn)換為使用局部變量解決問(wèn)題而不是共享變量解決,那將是極好的哈~。不過(guò)我貌似想不出什么案例來(lái)說(shuō)明一下,等想到了再說(shuō)哈,各位想到了也可以告訴我哈。
使用ThreadLocal類
為了維護(hù)一些線程內(nèi)可以共享的數(shù)據(jù),java提出了一個(gè)ThreadLocal類,它提供了下邊這些方法:
public class ThreadLocal{ protected T initialValue() { return null; } public void set(T value) { ... } public T get() { ... } public void remove() { ... } }
其中,類型參數(shù)T就代表了在同一個(gè)線程中共享數(shù)據(jù)的類型,它的各個(gè)方法的含義是:
T initialValue():當(dāng)某個(gè)線程初次調(diào)用get方法時(shí),就會(huì)調(diào)用initialValue方法來(lái)獲取初始值。
void set(T value):調(diào)用當(dāng)前線程將指定的value參數(shù)與該線程建立一對(duì)一關(guān)系(會(huì)覆蓋initialValue的值),以便后續(xù)get方法獲取該值。
T get():獲取與當(dāng)前線程建立一對(duì)一關(guān)系的值。
void remove():將與當(dāng)前線程建立一對(duì)一關(guān)系的值移除。
我們可以在同一個(gè)線程里的任何代碼處存取該類型的值:
public class ThreadLocalDemo { public static ThreadLocalTHREAD_LOCAL = new ThreadLocal (){ @Override protected String initialValue() { return "調(diào)用initialValue方法初始化的值"; } }; public static void main(String[] args) { ThreadLocalDemo.THREAD_LOCAL.set("與main線程關(guān)聯(lián)的字符串"); new Thread(new Runnable() { @Override public void run() { System.out.println("t1線程從ThreadLocal中獲取的值:" + ThreadLocalDemo.THREAD_LOCAL.get()); ThreadLocalDemo.THREAD_LOCAL.set("與t1線程關(guān)聯(lián)的字符串"); System.out.println("t1線程再次從ThreadLocal中獲取的值:" + ThreadLocalDemo.THREAD_LOCAL.get()); } }, "t1").start(); System.out.println("main線程從ThreadLocal中獲取的值:" + ThreadLocalDemo.THREAD_LOCAL.get()); } }
執(zhí)行結(jié)果是:
main線程從ThreadLocal中獲取的值:與main線程關(guān)聯(lián)的字符串 t1線程從ThreadLocal中獲取的值:調(diào)用initialValue方法初始化的值 t1線程再次從ThreadLocal中獲取的值:與t1線程關(guān)聯(lián)的字符串
從這個(gè)執(zhí)行結(jié)果我們也可以看出來(lái),不同線程操作同一個(gè) ThreadLocal 對(duì)象執(zhí)行各種操作而不會(huì)影響其他線程里的值。這一點(diǎn)非常有用,比如對(duì)于一個(gè)網(wǎng)絡(luò)程序,通常每一個(gè)請(qǐng)求都分配一個(gè)線程去處理,可以在ThreadLocal里記錄一下這個(gè)請(qǐng)求對(duì)應(yīng)的用戶信息,比如用戶名,登錄失效時(shí)間什么的,這樣就很有用了。
雖然ThreadLocal很有用,但是它作為一種線程級(jí)別的全局變量,如果某些代碼依賴它的話,會(huì)造成耦合,從而影響了代碼的可重用性,所以設(shè)計(jì)的時(shí)候還是要權(quán)衡一下子滴。
從可變性解決如果一個(gè)變量可以被共享,但是它自打被創(chuàng)建之后就不能被修改,那么隨意哪個(gè)線程去訪問(wèn)都可以哈,反正又不能改變它的值,隨便讀啦~
再?gòu)?qiáng)調(diào)一遍,我們寫的程序可能不僅我們自己會(huì)用,所以我們不能靠猜、靠直覺(jué)、靠信任其他使用我們寫的代碼的客戶端程序猿,所以如果我們想通過(guò)讓對(duì)象不可變的方式來(lái)保證線程安全,那就把該變量聲明為 final 的吧 :
public class FinalDemo { private final int finalField; public FinalDemo(int finalField) { this.finalField = finalField; } }
然后就可以隨便在多線程間共享finalField這個(gè)變量嘍~
加鎖解決鎖的概念
如果我們的需求確實(shí)是需要共享并且可變的變量,又想讓某些關(guān)于這個(gè)變量的操作是原子性的,還是以上邊的increase方法為例,我們現(xiàn)在面臨的困境是increase方法其實(shí)是由下邊3個(gè)原子性操作累積起來(lái)的一個(gè)操作:
讀變量i;
運(yùn)算;
寫變量i;
針對(duì)同一個(gè)變量i,不同線程可能交叉執(zhí)行上邊的三個(gè)步驟,導(dǎo)致兩個(gè)線程讀到同樣的變量i的值,從而導(dǎo)致結(jié)果比預(yù)期的小。為了讓increase方法里的操作具有原子性,也就是在一個(gè)線程執(zhí)行這一系列操作的同時(shí)禁止其他線程執(zhí)行這些操作,java提出了鎖的概念。
我們拿上廁所做一個(gè)例子,比如我們上廁所需要這幾步:
脫褲子
干正事兒
擦屁股
提褲子
上廁所的時(shí)候必須把這些步驟都執(zhí)行完了,才能圓滿的完成上廁所這個(gè)事兒,要不然執(zhí)行到擦屁股環(huán)節(jié)被別人趕出來(lái)豈不是賊尷尬
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74150.html
摘要:前言今天的筆記來(lái)了解一下原子操作以及中如何實(shí)現(xiàn)原子操作。概念原子本意是不能被進(jìn)一步分割的最小粒子,而原子操作意為不可被中斷的一個(gè)或一系列操作。處理器實(shí)現(xiàn)原子操作處理器會(huì)保證基本內(nèi)存操作的原子性。 showImg(https://segmentfault.com/img/bVVIRA?w=1242&h=536); 前言 今天的筆記來(lái)了解一下原子操作以及Java中如何實(shí)現(xiàn)原子操作。 概念 ...
摘要:的內(nèi)置鎖是一種互斥鎖,意味著最多只有一個(gè)線程能持有這種鎖。使用方式如下使用顯示鎖之前,解決多線程共享對(duì)象訪問(wèn)的機(jī)制只有和。后面會(huì)陸續(xù)的補(bǔ)充并發(fā)編程系列的文章。 早期的計(jì)算機(jī)不包含操作系統(tǒng),它們從頭到尾執(zhí)行一個(gè)程序,這個(gè)程序可以訪問(wèn)計(jì)算機(jī)中的所有資源。在這種情況下,每次都只能運(yùn)行一個(gè)程序,對(duì)于昂貴的計(jì)算機(jī)資源來(lái)說(shuō)是一種嚴(yán)重的浪費(fèi)。 操作系統(tǒng)出現(xiàn)后,計(jì)算機(jī)可以運(yùn)行多個(gè)程序,不同的程序在單獨(dú)...
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場(chǎng)景下會(huì)存在緩存一致性問(wèn)題。有沒(méi)有發(fā)現(xiàn),緩存一致性問(wèn)題其實(shí)就是可見(jiàn)性問(wèn)題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書中也都有關(guān)于這個(gè)知識(shí)點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說(shuō)自己更懵了。本文,就來(lái)整體的...
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場(chǎng)景下會(huì)存在緩存一致性問(wèn)題。有沒(méi)有發(fā)現(xiàn),緩存一致性問(wèn)題其實(shí)就是可見(jiàn)性問(wèn)題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書中也都有關(guān)于這個(gè)知識(shí)點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說(shuō)自己更懵了。本文,就來(lái)整體的...
閱讀 2044·2021-11-15 11:39
閱讀 3226·2021-10-09 09:41
閱讀 1491·2019-08-30 14:20
閱讀 3262·2019-08-30 13:53
閱讀 3325·2019-08-29 16:32
閱讀 3362·2019-08-29 11:20
閱讀 3018·2019-08-26 13:53
閱讀 775·2019-08-26 12:18