程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 高並發四:無鎖具體引見

Java 高並發四:無鎖具體引見

編輯:關於JAVA

Java 高並發四:無鎖具體引見。本站提示廣大學習愛好者:(Java 高並發四:無鎖具體引見)文章只能為提供參考,不一定能成為您想要的結果。以下是Java 高並發四:無鎖具體引見正文


在[高並發Java 一] 媒介中曾經提到了無鎖的概念,因為在jdk源碼中有年夜量的無鎖運用,所以在這裡引見下無鎖。

1 無鎖類的道理詳解

1.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指令完成的。

1.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傍邊供給了許多無鎖類,上面來引見下無鎖類。

2 無所類的應用

我們曾經曉得,無鎖比壅塞效力要高很多。我們來看看Java是若何完成這些無鎖類的。

2.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;
 }
 }

2.2 Unsafe

從類名便可知,Unsafe操作長短平安的操作,好比:

依據偏移量設置值(在方才引見的AtomicInteger中曾經看到了這個功效)
park()(把這個線程停上去,在今後的Blog中會提到)
底層的CAS操作
非地下API,在分歧版本的JDK中,能夠有較年夜差別

2.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("hosee", "ztk"))
 {
 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

可以看到只要一個線程可以或許修正值,而且前面的線程都不克不及再修正。

2.4.AtomicStampedReference

我們會發明CAS操作照樣有一個成績的

好比之前的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)
 {
 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>或許 Atomic Integer來完成就會形成屢次充值。

充值勝利,余額:39
花費10元,余額:29
花費10元,余額:19
充值勝利,余額:39
花費10元,余額:29
花費10元,余額:19
充值勝利,余額:39
花費10元,余額:29

2.5. AtomicIntegerArray

與AtomicInteger比擬,數組的完成不外是多了一個下標。

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

它的外部只是封裝了一個通俗的array

private final int[] array;

外面成心思的是應用了二進制數的前導零來算數組中的偏移量。

shift = 31 - Integer.numberOfLeadingZeros(scale);

前導零的意思就是好比8位表現12,00001100,那末前導零就是1後面的0的個數,就是4。

詳細偏移量若何盤算,這裡就不再做引見了。

2.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數值雷同,則解釋是線程平安的。

小解釋:

  1. Updater只能修正它可見規模內的變量。由於Updater應用反射獲得這個變量。假如變量弗成見,就會失足。好比假如某變量聲名為private,就是弗成行的。
  2. 為了確保變量被准確的讀取,它必需是volatile類型的。假如我們原有代碼中未聲名這個類型,那末簡略得聲名一下就行,這不會惹起甚麼成績。
  3. 因為CAS操作會經由過程對象實例中的偏移量直接停止賦值,是以,它不支撐static字段(Unsafe.objectFieldOffset()不支撐靜態變量)。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved