原子性與可見性


一、定義

1.可見性

在多核處理器中,如果多個線程對一個變量(假設)進行操作,但是這多個線程有可能被分配到多個處理器中運行,那么編譯器會對代碼進行優化,當線程要處理該變量時,多個處理器會將變量從主存復制一份分別存儲在自己的片上存儲器中,等到進行完操作后,再賦值回主存。(這樣做的好處是提高了運行的速度,因為在處理過程中多個處理器減少了同主存通信的次數);同樣在單核處理器中這樣由於“備份”造成的問題同樣存在!

這樣的優化帶來的問題之一是變量可見性——如果線程t1與線程t2分別被安排在了不同的處理器上面,那么t1與t2對於變量A的修改時相互不可見,如果t1給A賦值,然后t2又賦新值,那么t2的操作就將t1的操作覆蓋掉了,這樣會產生不可預料的結果。所以,即使有些操作時原子性的,但是如果不具有可見性,那么多個處理器中備份的存在就會使原子性失去意義。

2.原子性:

眾所周知,原子是構成物質的基本單位(當然電子等暫且不論),所以原子的意思代表着——“不可分”;

由不可分性可知,原子性是拒絕多線程操作的(只有分解為多步操作,多個線程才能對其操作:就像一個盒子里有多個兵乓球,多個人能夠從盒子里拿乒乓球;如果盒子只有一個兵乓球,一個人拿的話,其他人就拿不到了;這就是原子性,乒乓球就具有原子性,人就相當於原子)

 簡而言之——不被線程調度器中斷的操作,如:

賦值或者return。比如"a = 1;"和 "return a;"這樣的操作都具有原子性

原子性不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作!

3.非原子性操作

類似"a += b"這樣的操作不具有原子性,在某些JVM中"a += b"可能要經過這樣三個步驟:

(1)取出a和b

(2)計算a+b

(3)將計算結果寫入內存

如果有兩個線程t1,t2在進行這樣的操作。t1在第二步做完之后還沒來得及把數據寫回內存就被線程調度器中斷了,於是t2開始執行,t2執行完畢后t1又把沒有完成的第三步做完。這個時候就出現了錯誤,相當於t2的計算結果被無視掉了。所以上面的買碘片例子在同步add方法之前,實際結果總是小於預期結果的,因為很多操作都被無視掉了。

類似的,像"a++"這樣的操作也都不具有原子性。所以在多線程的環境下一定要記得進行同步操作

4.原子性與可見性的關系

原子性與可見性並沒有直接關聯的關系。說道這里,不得不要討論一下多線程帶來的問題及其本質。

(1)先來點廢話,有可能會將多核與單核處理器進行不同的區分,這里我搞混了,其實在代碼級別來說它們是相同的!

單核機器的多線程其實是為每個線程分配一個時間片段,所以實際上這些線程在微觀來說在一個時間段內只有一個在執行。這里產生的問題是如果一個線程操作一個內存空間然后突然被線程調度器終止掉(掛起),由另一個線程獲取CPU時間來對這個空間進行操作,那么着之間會產生不可預知的問題。

多核機器的基本原理與此是相同的,不同的是在同一時間,可能會有多個線程同時在進行操作(因為每個核心都可運行一項操作)。前面講到,多核機器由於多核的原因其多個線程對於相同內存的操作會產生可見性的問題。(可見性在單核和多核中同樣都存在)

(2)多線程中可見性造成的問題:

多個線程對相同變量的修改相互不可見,導致某部分操作被覆蓋,比如:

count++; t1與t2兩個線程准備操作它,當t1在自己存儲空間內修改完count值之后,並沒有及時將count修改回去,而是執行了count其它的操作——這時候,t2開始執行該操作,但是它並沒有發現count值進行了改變,這樣就造成了count值沒有被及時更新而產生的相關錯誤。

(3)其它問題:

同樣是count++語句,產生問題的語句還可能是其它原因造成的:t1與t2執行該語句,t1只比t2稍慢一點,t2修改后count,t1又將自己的結果寫入count,這樣t1的結果會對t2的結果進行覆蓋,這種覆蓋會造成一項不到的錯誤。

(1.2)非原子性造成的問題,多個線程在執行動作時某一方的“動作”“覆蓋”了另一方;

(5)討論:

可見性的問題造成了多線程的問題的一部分,確定變量的可見性只能解決一部分多線程的問題;而操作原子性是解決多線程的總的方法,因為它拒絕多個線程在同一時刻操作相同的一段內存。

 

5.volatile與synchronized關鍵字

(1)volatile

volatile賦予了變量可見——禁止編譯器對成員變量進行優化,它修飾的成員變量在每次被線程訪問時,都強迫從內存中重讀該成員變量的值;而且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存,這樣在任何時刻兩個不同線程總是看到某一成員變量的同一個值,這就是保證了可見性。文摘:

Java語言規范中指出:為了獲得最佳速度,允許線程保存共享成員變量的私有拷貝,而且只當線程進入或者離開同步代碼塊時才與共享成員變量
的原始值對比。這樣當多個線程同時與某個對象交互時,就必須要注意到要讓線程及時的得到共享成員變量的變化。而volatile關鍵字就是提示
VM:對於這個成員變量不能保存它的私有拷貝,而應直接與共享成員變量交互。
使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者為常量時,不必使用。
由於使用volatile屏蔽掉了VM中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。 就跟C中的一樣 禁止編譯器進行
優化~~~~

注意:

如果給一個變量加上volatile修飾符,就相當於:每一個線程中一旦這個值發生了變化就馬上刷新回主存,使得各個線程取出的值相同。編譯器不要對這個變量的讀、寫操作做優化。但是值得注意的是,除了對long和double的簡單操作之外,volatile並不能提供原子性。所以,就算你將一個變量修飾為volatile,但是對這個變量的操作並不是原子的,在並發環境下,還是不能避免錯誤的發生!

參考鏈接: http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

(2)synchronized

synchronized為一段操作或內存進行加鎖,它具有互斥性。當線程要操作被synchronized修飾的內存或操作時,必須首先獲得鎖才能進行后續操作;但是在同一時刻只能有一個線程獲得相同的一把鎖(對象監視器),所以它只允許一個線程進行操作。

簡單的理解方法:

synchronized(object) method();

這相當與為menthod()加了一把鎖,這把鎖就是object對象;當線程要訪問method方法時,需要獲取鑰匙:object的對象監視器,如果該鑰匙沒人拿走(之前沒有線程操作該方法或操作完成),則當前線程拿走鑰匙(獲取對象監視器),並操作方法;當操作完方法后,將“鑰匙”放回原處!

如果“鑰匙”不在原處,則該線程需要等待別人把鑰匙放回來(等待即進入阻塞狀態);如果多個線程要獲取該鑰匙,則它們需要進行“競爭”(一般是根據線程的優先級進行競爭)

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM