十二、Java高級特性(CAS基本原理)

一、什麼是原子操作?如何實現原子操作?

如果有兩個操作A和B,分別有兩個不同的線程執行。從A所在線程來看,執行B的線程執行B的時候要麼將B執行完畢,要麼完全不執行B,那麼A和B對彼此來說是原子的。

1、實現原子操作可以用鎖

鎖機制基本滿足了需求是沒有問題的。但是有時候我們需要更加有效,更加靈活,synchronized關鍵字是基於阻塞的鎖機制。也就是說當有一個線程擁有鎖的時候,訪問同一個資源的其他線程必須處於等待狀態。直到該線程釋放鎖。
這就會有一個問題:假如被阻塞的線程優先級很高怎麼辦?其次就是獲得鎖的線程一直不釋放鎖怎麼辦?還有就是如果有大量的線程來競爭資源,那麼CPU要花大量的時間和資源來處理這些競爭。同時還可能出現死鎖的情況。鎖是一種比較粗糙,粒度比較大的機制,對於一些像計數器這樣的需求,就顯得比較笨重。

2、實現原子操作還可以使用現代CPU支持的CAS指令

每一個CAS包含了三個運算符:
(1)內存地址V
(2)期望值A
(3)新值B

CAS舉例

現在我們有3個線程要對一個共享變量i實現+1的操作,理論上三個線程+1完最後的結果是3。我們可以使用synchronized鎖的機制來實現,同時也可以使用更加輕量的CAS指令實現。
下面我們介紹其原理:如下圖



(1)三個線程都是都可以進行i++,不需要阻塞等待,但是+1的時候並沒有真正的寫入內存。每個線程i++完之後,各自的i都是1。
(2)進入圖中的compare swap操作,比較和交換,這個操作屬於原子操作。即要麼全部執行,要麼不執行,因此在這每次只有一個線程執行或者不執行。假如三個線程分別爲A 、B、C。A線程進入原子操作比較和交換的時候,i的內存地址是V,期望值是i= 0,新值是i= 1。如果比較的時候i確實等於0,那麼進行交換,將i = 1,寫入內存,A線程原子操作完畢。B線程也一樣,i的內存地址是V,期望值是i= 0,新值是i= 1。此時B線程進行原子操作比較交換的時候發現i= 1,自己的期望值是0。他們倆不相等,因此從新來一次,將期望值置爲1,新值i= 1+1 =2。在進行原子操作比較和交換,這個時候如果C線程沒有經過原子操作修改過i的值,那麼此時B線程原子操作比較交換的時候,內存地址V中i的值是1,B的期望值是1,新值是2。內存V中地址的值和期望值一樣,因此交換。把新值i= 2寫入內存地址V中。當C線程進來的是也是一樣的道理。在原子操作的時候,當原子操作比較交換的時候,內存V中的值和期望值不一樣,那麼再來一次,將期望值修改。這個操作稱爲自旋。

二、實現原子操作的三大問題

1、ABA問題

因爲CAS需要在操作值的時候,檢查內存V中的值和自己的期望值是否相等,如果相等,則將新值寫入內存。但是並不知道內存V中的值中間是否發生了變化,例如當一個線程的期望值是A,但是內存地址V中的值,經過其他線程從A改成B,從B又改成A。當該線程進行CAS原子操作的時候,發現內存地址V中的值是A,和自己的期望值相等,那麼就直接將新值寫入到內存。然而它並不知道其實值已經經過多次的變化。
那麼如果對於需要知道A是否被更改過的要求,就會產生問題。解決這一問題,可以加一個版本號。也就是每次更改內存V中的值的時候,把版本號+1。通過拿到版本號就可以知道是否被更改過。

2、循環時間過長,開銷大。

自旋CAS如果時間太長,會給CPU帶來大的執行開銷。

3、只能保證一個共享變量的原子操作

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。
從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作

三、Jdk中相關原子操作類的使用

  • int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果。
  • boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
  • int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。
  • int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。
  • AtomicIntegerArray
    主要是提供原子的方式更新數組裏的整型,其常用方法如下。
  • int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。
  • boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
    需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。
  • 更新引用類型
    原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下3個類。
  • AtomicReference
  • 原子更新引用類型。
  • AtomicStampedReference
    利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作爲計數器使用,AtomicMarkableReference的pair使用的是boolean mark。 還是那個水的例子,AtomicStampedReference可能關心的是動過幾次,AtomicMarkableReference關心的是有沒有被人動過,方法都比較簡單。
  • AtomicMarkableReference:
    原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

四、AtomicInteger的使用舉例

package com.it.test.thread.consumer_product.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基本類型的原子操作
 */
public class CasTest {
    static AtomicInteger atomicInteger  = new AtomicInteger(10);
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                System.out.println(atomicInteger.getAndIncrement());//i++
            }
        }.start();
        new Thread(){
            @Override
            public void run() {

                System.out.println(atomicInteger.incrementAndGet());//++i
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                System.out.println( atomicInteger.getAndAdd(24));//i++
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                System.out.println(atomicInteger.addAndGet(30));//++i
            }
        }.start();




    }
}


10
12
12
66
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章