1. 程式人生 > >Java併發程式設計(八)------無鎖與無鎖類(原子操作類)

Java併發程式設計(八)------無鎖與無鎖類(原子操作類)

1. 無鎖的概念

無鎖主要有兩個特徵:

  • 是無障礙的
  • 保證有一個執行緒可以勝出

與無障礙相比,無障礙並不保證有競爭時一定能完成操作,因為如果它發現每次操作都會產生衝突,那它則會不停地嘗試。如果臨界區內的執行緒互相干擾,則會導致所有的執行緒會卡死在臨界區,那麼系統性能則會有很大的影響。

而無鎖增加了一個新的條件,保證每次競爭有一個執行緒可以勝出,則解決了無障礙的問題。至少保證了所有執行緒都順利執行下去,由於在jdk原始碼中有大量的無鎖應用。

2. 無鎖類的原理

2.1 CAS

CAS演算法的過程是這樣:它包含3個引數CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後,CAS返回當前V的真實值。CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。

我們會發現CAS的步驟很多,有沒有可能在判斷V和E相同後,正要賦值時,切換了執行緒,更改了值。造成了資料不一致呢?

事實上,這個擔心是多餘的。CAS整一個操作過程是一個原子操作,它是由一條CPU指令完成的。

2.2 CPU指令

CAS的CPU指令是cmpxchg

指令程式碼如下:

    /*
	accumulator = AL, AX, or EAX, depending on whether
	a byte, word, or doubleword comparison is being performed
	*/
	if(accumulator == Destination) {
	    ZF = 1;
	    Destination = Source;
	}else {
	    ZF = 0;
	    accumulator = Destination;
	}

目標值和暫存器裡的值相等的話,就設定一個跳轉標誌,並且把原始資料設到目標裡面去。如果不等的話,就不設定跳轉標誌了。Java當中提供了很多無鎖類,下面來介紹下無鎖類。

3. 無鎖類的使用

我們已經知道,無鎖比阻塞效率要高得多。我們來看看Java是如何實現這些無鎖類的。

3.1. AtomicInteger

AtomicInteger和Integer一樣,都繼承與Number類

public class AtomicInteger extends Number implements java.io.Serializable

AtomicInteger裡面有很多CAS操作,典型的有:

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

這裡來解釋一下unsafe.compareAndSwapInt方法,他的意思是,對於this這個類上的偏移量為valueOffset的變數值如果與期望值expect相同,那麼把這個變數的值設為update。其實偏移量為valueOffset的變數就是value。

static {
      try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
}

我們此前說過,CAS是有可能會失敗的,但是失敗的代價是很小的,所以一般的實現都是在一個無限迴圈體內,直到成功為止。

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

3.2 Unsafe

從類名就可知,Unsafe操作是非安全的操作,比如:

  • 根據偏移量設定值(在剛剛介紹的AtomicInteger中已經看到了這個功能)
  • park()(把這個執行緒停下來)
  • 底層的CAS操作

非公開API,在不同版本的JDK中,可能有較大差異。

3.3 AtomicReference

前面已經提到了AtomicInteger,當然還有AtomicBoolean,AtomicLong等等,都大同小異。

這裡要介紹的是AtomicReference,AtomicReference是一種模板類。

public class AtomicReference<V>  implements java.io.Serializable

它可以用來封裝任意型別的資料,比如String:

package test;

import java.util.concurrent.atomic.AtomicReference;

public class Test
{ 
	public final static AtomicReference<String> atomicString = new AtomicReference<String>("hosee");
	public static void main(String[] args)
	{
		for (int i = 0; i < 10; i++)
		{
			final int num = i;
			new Thread() {
				public void run() {
					try
					{
						Thread.sleep(Math.abs((int)Math.random()*100));
					}
					catch (Exception e)
					{
						e.printStackTrace();
					}
					if (atomicString.compareAndSet("a", "b"))
					{
						System.out.println(Thread.currentThread().getId() + "Change value");
					}else {
						System.out.println(Thread.currentThread().getId() + "Failed");
					}
				};
			}.start();
		}
	}
}

結果:

10Failed
13Failed
9Change value
11Failed
12Failed
15Failed
17Failed
14Failed
16Failed
18Failed

可以看到只有一個執行緒能夠修改值,並且後面的執行緒都不能再修改。 

3.4 AtomicStampedReference

我們會發現CAS操作還是有一個問題的(前面提到的"ABA")

比如之前的AtomicInteger的incrementAndGet方法

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

假設當前value=1當某執行緒int current = get()執行後,切換到另一個執行緒,這個執行緒將1變成了2,然後又一個執行緒將2又變成了1。此時再切換到最開始的那個執行緒,由於value仍等於1,所以還是能執行CAS操作,當然加法是沒有問題的,如果有些情況,對資料的狀態敏感時,這樣的過程就不被允許了。

此時就需要AtomicStampedReference類。

其內部實現一個Pair類來封裝值和時間戳

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

這個類的主要思想是加入時間戳來標識每一次改變

//比較設定 引數依次為:期望值 寫入新值 期望時間戳 新時間戳
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

當期望值等於當前值,並且期望時間戳等於現在的時間戳時,才寫入新值,並且更新新的時間戳。

這裡舉個用AtomicStampedReference的場景,可能不太適合,場景背景是,某公司給餘額少的使用者免費充值,但是每個使用者只能充值一次。

package test;

import java.util.concurrent.atomic.AtomicStampedReference;

public class Test
{
	static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);

	public static void main(String[] args)
	{
		for (int i = 0; i < 3; i++)
		{
			final int timestamp = money.getStamp();
			new Thread()
			{
				public void run()
				{
					while (true)
					{
						Integer m = money.getReference();
						if (m < 20)
						{
							if (money.compareAndSet(m, m + 20, timestamp,timestamp + 1))
							{
								System.out.println("充值成功,餘額:"+ money.getReference());
								break;
							}
						}
						else
						{
							break;
						}
					}
				};
			}.start();
		}

		new Thread()
		{
			public void run()
			{
				for (int i = 0; i < 100; i++)
				{
					while (true)
					{
						int timestamp = money.getStamp();
						Integer m = money.getReference();
						if (m > 10)
						{
							if (money.compareAndSet(m, m - 10, timestamp,timestamp + 1))
							{
								System.out.println("消費10元,餘額:"+ money.getReference());
								break;
							}
						}else {
							break;
						}
					}
					try
					{
						Thread.sleep(100);
					}
					catch (Exception e)
					{
						// TODO: handle exception
					}
				}
			};
		}.start();
	}

}

下面這段程式碼有3個執行緒在給使用者充值,當用戶餘額少於20時,就給使用者充值20元。有100個執行緒在消費,每次消費10元。使用者初始有9元,當使用AtomicStampedReference來實現時,只會給使用者充值一次,因為每次操作使得時間戳+1。執行結果:

充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
消費10元,餘額:9

如果使用AtomicReference<Integer>或者 AtomicInteger來實現就會造成多次充值。

充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
充值成功,餘額:39
消費10元,餘額:29

3.5 AtomicIntegerArray

與AtomicInteger相比,陣列的實現不過是多了一個下標

public final boolean compareAndSet(int i, int expect, int update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }

它的內部只是封裝了一個普通的array

private final int[] array;

裡面有意思的是運用了二進位制數的前導零來算陣列中的偏移量,前導零的意思就是比如8位表示12,00001100,那麼前導零就是1前面的0的個數,就是4。具體偏移量如何計算,這裡不再做具體介紹。

shift = 31 - Integer.numberOfLeadingZeros(scale);

3.6 AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater類的主要作用是讓普通變數也享受原子操作。

就比如原本有一個變數是int型,並且很多地方都應用了這個變數,但是在某個場景下,想讓int型變成AtomicInteger,但是如果直接改型別,就要改其他地方的應用。AtomicIntegerFieldUpdater就是為了解決這樣的問題產生的。

package test;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;


public class Test
{
	public static class V{
		int id;
		volatile int score;
		public int getScore()
		{
			return score;
		}
		public void setScore(int score)
		{
			this.score = score;
		}
		
	}
	public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");
	
	public static AtomicInteger allscore = new AtomicInteger(0);
	
	public static void main(String[] args) throws InterruptedException
	{
		final V stu = new V();
		Thread[] t = new Thread[10000];
		for (int i = 0; i < 10000; i++)
		{
			t[i] = new Thread() {
				@Override
				public void run()
				{
					if(Math.random()>0.4)
					{
						vv.incrementAndGet(stu);
						allscore.incrementAndGet();
					}
				}
			};
			t[i].start();
		}
		for (int i = 0; i < 10000; i++)
		{
			t[i].join();
		}
		System.out.println("score="+stu.getScore());
		System.out.println("allscore="+allscore);
	}
}

上述程式碼將score使用 AtomicIntegerFieldUpdater變成 AtomicInteger,從而保證了執行緒安全。

這裡使用allscore來驗證,如果score和allscore數值相同,則說明是執行緒安全的。

需要注意的是:

  • Updater只能修改它可見範圍內的變數。因為Updater使用反射得到這個變數。如果變數不可見,就會出錯。比如如果某變數申明為private,就是不可行的。
  • 為了確保變數被正確的讀取,它必須是volatile型別的。如果我們原有程式碼中未申明這個型別,那麼簡單得申明一下就行,這不會引起什麼問題。
  • 由於CAS操作會通過物件例項中的偏移量直接進行賦值,因此,它不支援static欄位(Unsafe.objectFieldOffset()不支援靜態變數)。

相關推薦

Java併發程式設計()------(原子操作)

1. 無鎖的概念 無鎖主要有兩個特徵: 是無障礙的 保證有一個執行緒可以勝出 與無障礙相比,無障礙並不保證有競爭時一定能完成操作,因為如果它發現每次操作都會產生衝突,那它則會不停地嘗試。如果臨界區內的執行緒互相干擾,則會導致所有的執行緒會卡死在臨界區,那麼系統性能則

Java併發程式設計的藝術》筆記三——的升級對比.md

0. 背景 Java SE1.6 為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級”鎖。 在Java SE 1.6 中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。這幾個狀態會隨著競爭情況依次升級。 鎖可以

Java併發程式設計(9):死(含程式碼)

JAVA大資料中高階架構 2018-11-10 14:04:32當執行緒需要同時持有多個鎖時,有可能產生死鎖。考慮如下情形: 執行緒A當前持有互斥所鎖lock1,執行緒B當前持有互斥鎖lock2。接下來,當執行緒A仍然持有lock1時,它試圖獲取lock2,因為執行緒B正持有lock2,因此執行緒A會阻塞等

【搞定Java併發程式設計】第10篇:的記憶體語義

上一篇:CAS詳解:https://blog.csdn.net/pcwl1206/article/details/84892287 目  錄: 1、鎖的釋放-獲取建立的happens-before關係 2、釋放鎖和獲取鎖的記憶體語義 3、鎖記憶體語義的實現 4、conc

Java併發程式設計 之 同步佇列等待佇列

在上一篇部落格中,我簡單的介紹了對Condition和ReentrantLock的使用,但是想要更好的掌握多執行緒程式設計,單單會用是不夠的。這篇我會針對Condition方法中的await和signal的實現原理來梳理一下我的理解。 首先我們需要了解同步佇列和等待佇列的概念。簡單的

java併發程式設計的藝術(一)---的基本屬性

這兩天在看《java併發程式設計的藝術》,記錄下看到的知識當做筆記吧! java中的synchronized鎖是儲存在物件頭中的,內容是mark word,長度根據計算機的位數來定32 or 64bit, 鎖一共有4種狀態,級別從低到高依次是:無鎖,偏向鎖,輕量級鎖,重量級鎖。鎖只能逐一升級,不能降級

java併發程式設計系列之ReadWriteLock讀寫的使用

前面我們講解了Lock的使用,下面我們來講解一下ReadWriteLock鎖的使用,顧明思義,讀寫鎖在讀的時候,上讀鎖,在寫的時候,上寫鎖,這樣就很巧妙的解決synchronized的一個性能問題:讀與讀之間互斥。 ReadWriteLock也是一個介面,原型如下: pub

Java併發程式設計系列之十二 死 飢餓和活

                        死鎖發生在一個執

java併發程式設計實戰:取消關閉筆記

在Java中無法搶佔式地停止一個任務的執行,而是通過中斷機制實現了一種協作式的方式來取消任務的執行。 設定取消標誌 public class MyTask implements Runnable { private final ArrayList<BigIntege

Java併發程式設計實戰(3)- 互斥

我們在這篇文章中主要討論如何使用互斥鎖來解決併發程式設計中的原子性問題。 [toc] # 概述 併發程式設計中的原子性問題的源頭是執行緒切換,那麼禁止執行緒切換可以解決原子性問題嗎? 這需要分情況討論,在單核CPU的情況下,同一時刻只有一個執行緒執行,禁止CPU中斷,就意味著作業系統不會重新排程執行緒,

Java併發程式設計實戰(4)- 死

在這篇文章中,我們主要討論一下死鎖及其解決辦法。 [toc] # 概述 在上一篇文章中,我們討論瞭如何使用一個互斥鎖去保護多個資源,以銀行賬戶轉賬為例,當時給出的解決方法是基於Class物件建立互斥鎖。 這樣雖然解決了同步的問題,但是能在現實中使用嗎?答案是不可以,尤其是在高併發的情況下,原因是我們使用

Java 併發程式設計(二):如何保證共享變數的原子性?

執行緒安全性是我們在進行 Java 併發程式設計的時候必須要先考慮清楚的一個問題。這個類在單執行緒環境下是沒有問題的,那麼我們就能確保它在多執行緒併發的情況下表現出正確的行為嗎? 我這個人,在沒有副業之前,一心撲在工作上面,所以處理的蠻得心應手,心態也一直保持的不錯;但有了副業之後,心態就變得像坐過山車一樣

Java併發筆記1-底層實現volatile、synchronized、原子操作

volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性:當一個執行緒修改一個變數時,另外一個執行緒能讀到這個修改的值。如果volatile變數修飾符使用恰當的話,他比synchronized的使用和執行成本更低,因為他不會引起

Java併發程式設計 -- 再論的問題 -- 優化

在前面JUC原始碼分析和Disruptor分析序列中,我們已經反覆討論了鎖與無鎖的問題。 眾所周知,在多執行緒程式中,鎖是效能殺手。因此“鎖優化”一直是多執行緒中被頻繁探討的一個問題。 本文將從“鎖優化”這個應用層面,把前面的諸多東西串起來,探討一下鎖優化的

Java併發程式設計高階技術-高效能併發框架原始碼解析實戰(密連結)

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級資料進行壓力測試。讓大

某課加密Java併發程式設計高階技術-高效能併發框架原始碼解析實戰

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級

Java併發程式設計高階技術-高效能併發框架原始碼解析實戰加密

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級

Java併發程式設計高階技術-高效能併發框架原始碼解析實戰加密分享

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級

Java併發程式設計高階技術-高效能併發框架原始碼解析實戰最新加密

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級

Java併發程式設計》學習 --4.4

對於併發控制,鎖是一種悲觀的策略。它總是假設每一次的臨界區操作會產生衝突。如果有多個執行緒同時需要訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待,所以說鎖會阻塞執行緒執行。而無鎖是一種樂觀的策略,它會假設對資源的訪問是沒有衝突的。無鎖的策略使用一種叫做比較交換的技術(CAS