国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

基礎知識-cas,synchronized,lock,volatile,concurrent大集合

sf190404 / 1865人閱讀

摘要:加鎖,多線程為了防止競爭資源,即防止對同一資源進行并發操作。釋放占有的對象鎖,線程進入等待池,釋放而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序。休眠結束,線程重新獲得執行代碼。則是喚醒所有等待的線程。

先了解一下java 模型

(1)每個線程都有自己的本地內存空間(java棧中的幀)。線程執行時,先把變量從內存讀到線程自己的本地內存空間,然后對變量進行操作。
(2)對該變量操作完成后,在某個時間再把變量刷新回主內存。

那么我們再了解下鎖提供的兩種特性:互斥(mutual exclusion) 和可見性(visibility):
(1)互斥(mutual exclusion):互斥即一次只允許一個線程持有某個特定的鎖,因此可使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程能夠使用該共享數據;
(2)可見性(visibility):簡單來說就是一個線程修改了變量,其他線程可以立即知道。保證可見性的方法:volatile,synchronized,final(一旦初始化完成其他線程就可見)。

加鎖,多線程為了防止競爭資源,即防止對同一資源進行并發操作。
鎖機制存在以下問題
(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。
獨占鎖是一種悲觀鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
樂觀鎖是每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

volatile 2個特征

可見性:一個線程修改了某個共享變量的值,其他線程能夠立馬得知這個修改。

 1.當寫一個volatile變量的時候,JMM會把本地內存中的共享變量刷新到主內存。
 2.當讀一個volatile變量的是時候,JMM會把線程本地內存的值設置為無效,然后從主內存中讀取共享變量。

禁止特定的處理器重排序

 1.當第二個操作為volatile寫的時候,第一個操作不管是什么,都不允許重排序。
 2.當第一個操作為volatile讀的時候,第二個操作不管是什么,都不允許重排序。
 3.當第一個操作為volatile寫的時候,第二個操作是volatile讀的時候,不允許重排序。

除此以外的情況,都運行重排序。而重排序的實現是靠加入內存屏障來實現的。內存屏障時用來禁止特定的重排序的cpu指令。包括4中,loadload,store store,store load與load/store。load可以理解為讀操作,store可以理解為寫操作,舉例說明,loadload是保證在第二個load和其他一系列操作之前要確保第一個load的讀操作完成。store store是保證在第二個store及寫操作之前,第一個store寫操作對其他處理器可見。其中store load的開銷最大,是個萬能屏障,兼具其他三個屏障的功能。

public class RunThread extends Thread {

    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("線程執行完成了");
    }
}

public class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在main線程中,thread.setRunning(false);將啟動的線程RunThread中的共享變量設置為false,從而想讓RunThread.java的while循環結束。如果使用JVM -server參數執行該程序時,RunThread線程并不會終止,從而出現了死循環。

原因分析
現在有兩個線程,一個是main線程,另一個是RunThread。它們都試圖修改isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改后,再刷新回主內存。
而在JVM設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。因此,RunThread線程無法讀到main線程改變的isRunning變量。從而出現了死循環,導致RunThread無法終止。

解決方法

volatile private boolean isRunning = true;

原理
當對volatile標記的變量進行修改時,會將其他緩存中存儲的修改前的變量清除,然后重新讀取。一般來說應該是先在進行修改的緩存A中修改為新值,然后通知其他緩存清除掉此變量,當其他緩存B中的線程讀取此變量時,會向總線發送消息,這時存儲新值的緩存A獲取到消息,將新值穿給B。最后將新值寫入內存。當變量需要更新時都是此步驟,volatile的作用是被其修飾的變量,每次更新時,都會刷新上述步驟。

synchronized

(1)synchronized 方法
方法聲明時使用,放在范圍操作符(public等)之后,返回類型聲明(void等)之前.這時,線程獲得的是成員鎖,即一次只能有一個線程進入該方法,其他線程要想在此時調用該方法,只能排隊等候,當前線程(就是在synchronized方法內部的線程)執行完該方法后,別的線程才能進入。

示例:

public synchronized void synMethod(){
}

如在線程t1中有語句obj.synMethod(); 那么由于synMethod被synchronized修飾,在執行該語句前, 需要先獲得調用者obj的對象鎖, 如果其他線程(如t2)已經鎖定了obj (可能是通過obj.synMethod,也可能是通過其他被synchronized修飾的方法obj.otherSynMethod鎖定的obj), t1需要等待直到其他線程(t2)釋放obj, 然后t1鎖定obj, 執行synMethod方法. 返回之前之前釋放obj鎖。

(2)synchronized 塊
對某一代碼塊使用,synchronized后跟括號,括號里是變量,這樣,一次只有一個線程進入該代碼塊.此時,線程獲得的是成員鎖。

(3)synchronized (this)
當兩個并發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
  
當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。  

然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的除synchronized(this)同步代碼塊以外的部分。 

第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。  

以上規則對其它對象鎖同樣適用。

第三點舉例說明:

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}

含有synchronized同步塊的方法m4t1被訪問時,線程中m4t2()依然可以被訪問。

wait()sleep() notify()/notifyAll()
wait():釋放占有的對象鎖,線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序。
sleep():不同的是,線程調用此方法后,會休眠一段時間,休眠期間,會暫時釋放cpu,但并不釋放對象鎖。也就是說,在休眠期間,其他線程依然無法進入此代碼內部。休眠結束,線程重新獲得cpu,執行代碼。
wait()和sleep()最大的不同在于wait()會釋放對象鎖,而sleep()不會
notify(): 該方法會喚醒因為調用對象的wait()而等待的線程,其實就是對對象鎖的喚醒,從而使得wait()的線程可以有機會獲取對象鎖。調用notify()后,并不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢,才會釋放對象鎖。JVM則會在等待的線程中調度一個線程去獲得對象鎖,執行代碼。需要注意的是,wait()和notify()必須在synchronized代碼塊中調用。
notifyAll()則是喚醒所有等待的線程。

lock

synchronized的缺陷
1)synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那么為什么會出現Lock呢?
如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
  1)獲取鎖的線程執行完了該代碼塊,然后線程釋放對鎖的占有;
  2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。
  那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能等待,試想一下,這多么影響程序執行效率。
  因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。
再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。
但是采用synchronized關鍵字來實現同步的話,就會導致一個問題:
如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。
因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
  1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
  2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

(2)java.util.concurrent.locks包下常用的類

public interface Lock {
    //獲取鎖,如果鎖被其他線程獲取,則進行等待
    void lock(); 

    //當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
    void lockInterruptibly() throws InterruptedException;

    /**tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成
    *功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回
    *false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。*/
    boolean tryLock();

    //tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock(); //釋放鎖
    Condition newCondition();
}

通常使用lock進行同步:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
    lock.unlock();   //釋放鎖
}

trylock使用方法:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){

     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意:
當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過多帶帶調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
而用synchronized修飾的話,當一個線程處于等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

(3)ReentrantLock
ReentrantLock,意思是“可重入鎖”,是唯一實現了Lock接口的類,并且ReentrantLock提供了更多的方法。

public class Test {
    private ArrayList arrayList = new ArrayList();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  

    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。

代碼解釋:

class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
  而由于synchronized和Lock都具備可重入性,所以不會發生上述現象。

volatile和synchronized區別
  1)volatile本質是在告訴jvm當前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住.
  2)volatile僅能使用在變量級別,synchronized則可以使用在變量,方法.
  3)volatile僅能實現變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性.
  《Java編程思想》上說,定義long或double變量時,如果使用volatile關鍵字,就會獲得(簡單的賦值與返回操作)原子性。
  4)volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.
  5)當一個域的值依賴于它之前的值時,volatile就無法工作了,如n=n+1,n++等。如果某個域的值受到其他域的值的限制,那么volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
  6)使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域。

synchronized和lock區別
  1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
  2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
  3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
  4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
  5)Lock可以提高多個線程進行讀操作的效率。
  在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優于synchronized。所以說,在具體使用時要根據適當情況選擇。

CAS
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
非阻塞算法 (nonblocking algorithms)

一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。
現代的CPU提供了特殊的指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。
拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數據正確性的。

private volatile int value;

首先毫無以為,在沒有鎖的機制下可能需要借助volatile原語,保證線程間的數據是可見的(共享的)。
這樣才獲取變量的值的時候才能直接讀取。

public final int get() {
??????? return value;
??? }

然后來看看++i是怎么做到的。

public final int incrementAndGet() {
??? for (;;) {
??????? int current = get();
??????? int next = current + 1;
??????? if (compareAndSet(current, next))
??????????? return next;
??? }
}

在這里采用了CAS操作,每次從內存中讀取數據然后將此數據和+1后的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。
而compareAndSet利用JNI來完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {???
??? return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
??? }

整體的過程就是這樣子的,利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。
其中

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

類似:

if (this ==?expect) {

? this =?update

?return true;

} else {

return false;

}

那么問題就來了,成功過程中需要2個步驟:比較this ==?expect,替換this =?update,compareAndSwapInt如何這兩個步驟的原子性呢? 參考CAS的原理。

CAS原理

CAS通過調用JNI的代碼實現的。JNI:Java Native Interface為JAVA本地調用,允許java調用其他語言。
而compareAndSwapInt就是借助C來調用CPU底層指令實現的。
Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現位于unsafe.cpp中

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

先想辦法拿到變量value在內存中的地址。
通過Atomic::cmpxchg實現比較替換,其中參數x是即將更新的值,參數e是原內存的值。

下面從分析比較常用的CPU(intel x86)來解釋CAS的實現原理。

下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

可以看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼為:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(對應于windows操作系統,X86處理器)。下面是對應于intel x86處理器的源代碼的片段:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn"t like the lock prefix to be on a single line
// so we can"t insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  
                       __asm je L0      
                       __asm _emit 0xF0 
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
/ alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

如上面源代碼所示,程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。

intel的手冊對lock前綴的說明如下:

確保對內存的讀-改-寫操作原子執行。在Pentium及Pentium之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處于獨占或以修改狀態),并且該內存區域被完全包含在單個緩存行(cache line)中,那么處理器將直接執行該指令。由于在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
禁止該指令與之前和之后的讀和寫指令重排序。
把寫緩沖區中的所有數據刷新到內存中。
在這里可以看到是用嵌入的匯編實現的, 關鍵CPU指令是?cmpxchg
到這里沒法再往下找代碼了. 也就是說CAS的原子性實際上是CPU實現的. 其實在這一點上還是有排他鎖的. 只是比起用synchronized, 這里的排他時間要短的多. 所以在多線程情況下性能會比較好.

代碼里有個alternative?for?InterlockedCompareExchange
這個InterlockedCompareExchange是WINAPI里的一個函數, 做的事情和上面這段匯編是一樣的
http://msdn.microsoft.com/en-...

最后再貼一下x86的cmpxchg指定

  Opcode CMPXCHG
   
    CPU: I486+?
    Type of Instruction: User?
    
    Instruction: CMPXCHG dest, src?
    
    Description: Compares the accumulator with dest. If equal the "dest"?
    is loaded with "src", otherwise the accumulator is loaded?
    with "dest".?
    
    Flags Affected: AF, CF, OF, PF, SF, ZF?
    
    CPU mode: RM,PM,VM,SMM?
    +++++++++++++++++++++++?
    Clocks:?
    CMPXCHG reg, reg 6?
    CMPXCHG mem, reg 7 (10 if compartion fails)

關于CPU的鎖有如下3種:
處理器自動保證基本內存操作的原子性
  首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。?

使用總線鎖保證原子性
  第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。如下圖

原因是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操作,然后分別寫入系統內存當中。那么想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。
  處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占使用共享內存。

使用緩存鎖保證原子性
  第二個機制是通過緩存鎖定保證原子性。在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

  頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,并不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現復雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那么CPU2就不能同時緩存了i的緩存行。

  但是有兩種情況下處理器不會使用緩存鎖定。第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對于Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

  以上兩個機制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。

CAS缺點
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作

ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。

關于ABA問題參考文檔:?http://blog.hesey.net/2011/09...

循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決于具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。

concurrent包的實現
由于java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:

A線程寫volatile變量,隨后B線程讀這個volatile變量。
A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

首先,聲明共享變量為volatile;
然后,使用CAS的原子條件更新來實現線程之間的同步;
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴于這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:

參考
https://blog.csdn.net/ztchun/...
http://www.cnblogs.com/imqsl/...

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/71745.html

相關文章

  • 高并發 - 基礎

    摘要:異步非阻塞方式,任務的完成的通知由其他線程發出。并發并行死鎖饑餓活鎖死鎖線程持有,線程持有。如等,在多線程情況下,該操作不是原子級別的而是原子的,所以一般用于狀態標記。 同步/異步、阻塞/非阻塞 同步/異步是 API 被調用者的通知方式。阻塞/非阻塞則是 API 調用者的等待方式(線程掛機/不掛起)。 同步非阻塞 Future方式,任務的完成要主線程自己判斷。如NIO,后臺有多個任務在...

    phpmatt 評論0 收藏0
  • 深入理解Java內存模型(五)——鎖

    摘要:前情提要深入理解內存模型四鎖的釋放獲取建立的關系鎖是并發編程中最重要的同步機制。鎖內存語義的實現本文將借助的源代碼,來分析鎖內存語義的具體實現機制。請看下篇深入理解內存模型六 前情提要 深入理解Java內存模型(四)—— volatile 鎖的釋放-獲取建立的happens before 關系 鎖是java并發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向...

    caige 評論0 收藏0
  • Java中的鎖

    摘要:當前線程在超時時間內被中斷超時時間結束,返回釋放鎖獲取等待通知組件,該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的方法,調用后,當前線程將釋放鎖。同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。 本文在參考java并發編程實戰后完成,參考內容較多 Java中的鎖 鎖是用來控制多線程訪問共享資源的方式,一個鎖能夠防止多個線程同事訪問共享資源。在Lock...

    gaara 評論0 收藏0
  • Java - 并發 atomic, synchronization and volatile

    摘要:線程的這種交叉操作會導致線程不安全。原子操作是在多線程環境下避免數據不一致必須的手段。如果聲明一個域為一些情況就可以確保多線程訪問到的變量是最新的。并發要求一個線程對對象進行了操作,對象發生了變化,這種變化應該對其他線程是可見的。 雖是讀書筆記,但是如轉載請注明出處 http://segmentfault.com/blog/exploring/ .. 拒絕伸手復制黨 一個問題: ...

    StonePanda 評論0 收藏0
  • Java并發編程-原子操作

    摘要:這個規則比較好理解,無論是在單線程環境還是多線程環境,一個鎖處于被鎖定狀態,那么必須先執行操作后面才能進行操作。線程啟動規則獨享的方法先行于此線程的每一個動作。 1. 指令重排序 關于指令重排序的概念,比較復雜,不好理解。我們從一個例子分析: public class SimpleHappenBefore { /** 這是一個驗證結果的變量 */ private st...

    SillyMonkey 評論0 收藏0

發表評論

0條評論

sf190404

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<