摘要:在多線程的問題上面概念比較多,也需要慢慢理解,其實(shí)也在多線程的鎖的上面做了很多優(yōu)化,還有互斥同步和非互斥同步,還有很多概念,什么是自旋和自適應(yīng)自旋,鎖消除順便提一下,上面的字符串拼接的例子就是用到了這種優(yōu)化方式,鎖粗化,我們下次再繼續(xù)分享。
在我們平常的開發(fā)工作中,或多或少的都能接觸到多線程編程或者一些并發(fā)問題,隨著操作系統(tǒng)和系統(tǒng)硬件的升級(jí),并發(fā)編程被越來(lái)越多的運(yùn)用到我們的開發(fā)中,我們使用多線程的最初的想法是能夠更大程度的利用系統(tǒng)資源,但是我們?cè)谑褂枚嗑€程的時(shí)候,也會(huì)有一些問題的存在,我們先來(lái)看一段代碼。
private static int i = 0; private static void increse(){ i++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for (int i = 0; i < threads.length; i++){ threads[i] = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++){ increse(); } } }); threads[i].start(); } while (Thread.activeCount() > 1){ Thread.yield(); } System.out.println(i); }
首先看看這段代碼是沒有問題的,但是如果在多線程的環(huán)境中,這段代碼運(yùn)行的結(jié)果基本是都不一樣的,這里是開啟20個(gè)線程,然后每一個(gè)線程調(diào)用increse()方法對(duì)變量i進(jìn)行一個(gè)賦值操作,預(yù)期的一個(gè)輸出應(yīng)該是200000,但是為什么會(huì)每一次的輸出都不太一樣呢?原因就在于這個(gè)地方i++,這里就是產(chǎn)生并發(fā)問題的根本原因,那看起來(lái)很簡(jiǎn)單的一個(gè)i++為什么會(huì)有這種問題?這里就簡(jiǎn)單的從java內(nèi)存模型(JMM)來(lái)了解一下,首先JMM規(guī)定,每一個(gè)線程運(yùn)行時(shí)都會(huì)有一個(gè)工作內(nèi)存,然后變量i是存儲(chǔ)在主內(nèi)存的,每次線程在計(jì)算數(shù)據(jù)的時(shí)候都要去主內(nèi)存中獲取當(dāng)前變量的值,那么簡(jiǎn)單的來(lái)說(shuō),就是一個(gè)線程將變量i計(jì)算得到結(jié)果后,還沒有將這個(gè)數(shù)據(jù)刷新到主內(nèi)存,在這個(gè)時(shí)候,其他的線程已經(jīng)獲取到了原來(lái)的值,換句話說(shuō),本線程中獲取到的數(shù)據(jù)是否是最新的,這個(gè)是不知道的。但是這只是從JMM角度來(lái)簡(jiǎn)單的說(shuō)一下,i++這個(gè)看似簡(jiǎn)單的操作其實(shí)包含了三個(gè)操作,獲取i的值,對(duì)i進(jìn)行自增,然后對(duì)i進(jìn)行賦值。就是在這幾個(gè)操作中,其他的線程會(huì)有很多的時(shí)間來(lái)做很多的事情,那有人會(huì)問,是不是將這個(gè)i++操作同步就可以了?是的,那該如何同步呢?
有人說(shuō)用volatile來(lái)修飾一下變量i不就可以了么?是的,java中volatile這個(gè)關(guān)鍵字確實(shí)是提供了一個(gè)同步的功能,但是為什么在這里修改一下還是沒有效果呢?原因就在于如果一個(gè)變量用volatile修飾之后,只是會(huì)讓其他的線程立即能夠知道當(dāng)前變量的值是多少,這里就叫做可見性,但是還是解決不了i++這幾個(gè)操作的問題,那如何處理,又有人提出用synchronized這個(gè)關(guān)鍵字,不得不承認(rèn),這個(gè)關(guān)鍵字確實(shí)很強(qiáng)大,是能夠解決這個(gè)問題,那我們有沒有考慮過(guò)這個(gè)為什么可以解決這個(gè)問題,是如何解決的,那還是簡(jiǎn)單的說(shuō)一下,首先synchronized是屬于JVM級(jí)別的,有這個(gè)關(guān)鍵字的方法或者代碼塊,最后會(huì)被解釋成monitorenter和monitorexit指令,這兩個(gè)字節(jié)碼都明確需要一個(gè)reference類型的參數(shù)來(lái)指出要鎖定或者解鎖的對(duì)象,像這樣:
public synchronized String f(){ //code } synchronized(object){ //code }
看到這里,我們應(yīng)該能明白synchronized為什么可以解決上面程序的問題,但是我們還應(yīng)該要明確一個(gè)概念就是原子性,換句話說(shuō),就是我們?cè)谔幚硪恍┒嗑€程的問題的時(shí)候,應(yīng)該保證一些共享數(shù)據(jù)的操作是原子性的,這樣才能保證正確性,看到這里,相信你也有了一個(gè)大概的理解,那我們來(lái)總結(jié)一下,在處理多線程的問題的時(shí)候,哪些點(diǎn)是值得注意的,可見性,原子性,有序性,這幾個(gè)點(diǎn)是保證多線程能夠正確的一個(gè)前提條件,至于什么是有序性,這里涉及到內(nèi)存指令的重排序,不在討論范圍內(nèi),以后再來(lái)討論。
這里還要指出一個(gè)問題,就是是否我們?cè)谔幚矶嗑€程問題的時(shí)候,一定要同步,或者說(shuō)一定要加鎖,這個(gè)也不是一定的,之前網(wǎng)上有一個(gè)說(shuō)笑的方式,就是我們?cè)谔幚矶嗑€程的問題的時(shí)候,有時(shí)候就會(huì)發(fā)現(xiàn),代碼又被寫成了單線程,當(dāng)然這只是一個(gè)玩笑話,但是這里我們也能看出來(lái),是不是單線程的程序就不會(huì)有這些問題?答案是肯定的,因?yàn)閱尉€程不存在資源競(jìng)爭(zhēng)的問題,也就不需要再討論了。
那么我們什么時(shí)候需要使用同步,什么時(shí)候又不需要呢?我們來(lái)看一段代碼
public String f(String s1, String s2, String s3){ return s1 + s2 +s3; }
這是一個(gè)字符串拼接的一個(gè)方法,我們來(lái)反編譯看一下,這里JVM到底是怎么做的?
這里很明顯的能夠看出來(lái),最后是通過(guò)StringBuilder來(lái)為我們生成了最后的結(jié)果,那有人會(huì)問,這里線程安全么?是的,這里是線程安全的,因?yàn)樵谶@個(gè)方法中,雖然也有變量的使用,但是都是屬于線程內(nèi)部在使用,其他的線程根本不會(huì)訪問到或者說(shuō)這些變量也不會(huì)讓其他線程訪問到,我們稱其為沒有方法逃逸,也就是說(shuō)只能在本線程中使用這些變量,這里是線程安全的,至于什么是逃逸分析,簡(jiǎn)單的提一下就是這是JVM的高級(jí)優(yōu)化的一種方式,說(shuō)的再簡(jiǎn)單一點(diǎn),就是別的線程訪問不到這個(gè)變量,這樣的代碼是不需要同步的。
在多線程的問題上面概念比較多,也需要慢慢理解,其實(shí)JVM也在多線程的鎖的上面做了很多優(yōu)化,還有互斥同步和非互斥同步,還有很多概念,什么是自旋和自適應(yīng)自旋,鎖消除(順便提一下,上面的字符串拼接的例子就是用到了這種優(yōu)化方式),鎖粗化,我們下次再繼續(xù)分享。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/76658.html