天天看點

Java中Atomic原子類型的詳細講解(一)-劉宇一、什麼是原子類型二、原子類型的實作原理三、利用volatile關鍵字示範原子性問題四、利用AtomicInteger解決原子性問題五、AtomicInteger的基本使用六、AtomicBoolean的基本使用七、AtomicBoolean的作用八、AtomicLong的基本使用九、compareAndSwap算法(CAS)詳解十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象

Java中Atomic原子類型的詳細講解(一)-劉宇

  • 一、什麼是原子類型
  • 二、原子類型的實作原理
  • 三、利用volatile關鍵字示範原子性問題
    • 示範
    • 為什麼多個線程會輸出相同的結果
  • 四、利用AtomicInteger解決原子性問題
  • 五、AtomicInteger的基本使用
    • 1、AtomicInteger的建立和get()方法
    • 2、set()
    • 3、getAndSet(int)
    • 4、getAndAdd(int)
    • 5、addAndGet(int)
    • 6、getAndIncrement()
    • 7、incrementAndGet()
    • 9、getAndDecrement()
    • 10、decrementAndGet()
    • 11、compareAndSet(int expect, int update)
  • 六、AtomicBoolean的基本使用
    • 1、AtomicInteger的建立和get()方法
    • 2、set()
    • 3、getAndSet(int)
    • 4、compareAndSet(boolean expect, boolean update)
  • 七、AtomicBoolean的作用
  • 八、AtomicLong的基本使用
    • AtomicLong與AtomicInteger的差別
    • VMSupportsCS8()方法的作用
  • 九、compareAndSwap算法(CAS)詳解
  • 十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象

作者:劉宇

CSDN部落格位址:https://blog.csdn.net/liuyu973971883

有部分資料參考,如有侵權,請聯系删除。如有不正确的地方,煩請指正,謝謝。

一、什麼是原子類型

  • 原子操作是指不會被線程排程機制打斷的操作,這種操作一旦開始,就一直運作到結束,中間不會有任何線程上下文切換。
  • Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。
  • Atomic包大緻可以屬于4種類型的原子更新方式,分别是
    1. 原子更新基本類型
    2. 原子更新數組
    3. 原子更新引用
    4. 原子更新屬性

二、原子類型的實作原理

Atomic包裡的類基本都是使用Unsafe實作的包裝類,進而達到了原子性的操作。然後通過将内部的value變量用volatile關鍵字修飾,進而達到了可見性、防止重排序、原子性。

三、利用volatile關鍵字示範原子性問題

示範

使用三個線程分别對同一個變量進行加1操作,原本理想的結果應該是輸出1500個元素,而真實結果卻隻輸出了1498個,原因是有線程輸出了同樣的結果。是因為volatile關鍵字可以添加記憶體屏障有效防止重排序、記憶體可見性,但是不能確定其原子性,而value+=1時,看似是一行代碼,實則是分成了多步執行的。

package com.brycen.concurrency03.atomic;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
	private static volatile int value = 0;
	private static Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
	
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value += 1;
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
			
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value += 1;
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
			
		});
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value += 1;
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
			
		});
		t1.start();
		t2.start();
		t3.start();
		t1.join();
		t2.join();
		t3.join();
	}
}
           

輸出結果:

我們可以看到,原本應該輸出1500個,但是結果卻隻輸出了1498個。這是因為volatile關鍵字可以添加記憶體屏障有效防止重排序、記憶體可見性,但是不能確定其原子性,而value+=1時,看似是一行代碼,實則是分成了多步執行的

Java中Atomic原子類型的詳細講解(一)-劉宇一、什麼是原子類型二、原子類型的實作原理三、利用volatile關鍵字示範原子性問題四、利用AtomicInteger解決原子性問題五、AtomicInteger的基本使用六、AtomicBoolean的基本使用七、AtomicBoolean的作用八、AtomicLong的基本使用九、compareAndSwap算法(CAS)詳解十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象

為什麼多個線程會輸出相同的結果

volatlie關鍵字沒有原子性,那麼value+=1這行代碼實則會被分為4步執行

  1. 擷取value的值
  2. 将擷取到的值+1
  3. 将最新值指派給value
  4. 将value的值刷入記憶體

假設當時value值為1,當線程1執行完+=操作的第1步時,cpu執行權被線程2搶走,然後線程2執行+=操作,直至輸入記憶體,并輸出2,這時cpu執行被線程1搶走,繼續執行沒有完成的+=操作,那麼這時線程1會根據第一步拿到的1進行+1操作,那麼傳回輸出的同樣是2。

四、利用AtomicInteger解決原子性問題

package com.brycen.concurrency03.atomic;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
	private static Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
	private static AtomicInteger value = new AtomicInteger();
    
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value.getAndIncrement();
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
			
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value.getAndIncrement();
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
			
		});
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int x= 0;
				while(x<500) {
					int temp = value.getAndIncrement();
					set.add(temp);
					System.out.println(Thread.currentThread().getName()+":"+temp);
					x++;
				}
			}
		});
		
		
		t1.start();
		t2.start();
		t3.start();
		t1.join();
		t2.join();
		t3.join();
	}
}
           

輸出結果:

Java中Atomic原子類型的詳細講解(一)-劉宇一、什麼是原子類型二、原子類型的實作原理三、利用volatile關鍵字示範原子性問題四、利用AtomicInteger解決原子性問題五、AtomicInteger的基本使用六、AtomicBoolean的基本使用七、AtomicBoolean的作用八、AtomicLong的基本使用九、compareAndSwap算法(CAS)詳解十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象

五、AtomicInteger的基本使用

1、AtomicInteger的建立和get()方法

  • AtomicInteger的建立分為兩種:
    1. 無參的,預設值0
    2. 有參的,指定預設值
  • get():用于擷取目前值,該方法是不需要鎖的,因為他隻是去取值
AtomicInteger i = new AtomicInteger();
System.out.println(i.get());//輸出0
AtomicInteger j = new AtomicInteger(10)
System.out.println(j.get());//輸出10
           

2、set()

  • 該方法是不需要鎖的,因為set方法相當于初始化該變量了
AtomicInteger i = new AtomicInteger();
i.set(12);
System.out.println(i.get());//輸出12
           

3、getAndSet(int)

  • 先取值,再設定值。
AtomicInteger i = new AtomicInteger();
int result = i.getAndSet(10);
System.out.println(result);//輸出0
System.out.println(i.get());//輸出10
           

4、getAndAdd(int)

  • 先拿到值并且後加num。
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndAdd(10);
System.out.println(result);//輸出10
System.out.println(i.get());//輸出20
           

5、addAndGet(int)

  • 先自動加num,再取值。
AtomicInteger i = new AtomicInteger(10);
int result = i.addAndGet(10);
System.out.println(result);//輸出20
System.out.println(i.get());//輸出20
           

6、getAndIncrement()

  • 先拿到值然後再+1
AtomicInteger i = new AtomicInteger();
int result = i.getAndIncrement();
System.out.println(result);//輸出0
System.out.println(i.get());//輸出1
           

7、incrementAndGet()

  • 先拿到值然後再+1
AtomicInteger i = new AtomicInteger();
int result = i.incrementAndGet();
System.out.println(result);//輸出1
System.out.println(i.get());//輸出1
           

9、getAndDecrement()

  • 先拿到值然後再-1
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndDecrement();
System.out.println(result);//輸出10
System.out.println(i.get());//輸出9
           

10、decrementAndGet()

  • 先-1再取值
AtomicInteger i = new AtomicInteger();
int result = i.decrementAndGet();
System.out.println(result);//輸出9
System.out.println(i.get());//輸出9
           

11、compareAndSet(int expect, int update)

  • 快速失敗政策,是用于判斷期望值是否與變量實際值相等,如果相等則将update指派給變量,否則失敗。
//成功案例
AtomicInteger atomicInteger = new AtomicInteger(10);
boolean result = atomicInteger.compareAndSet(10, 12);
System.out.println(result);//輸出true
System.out.println(atomicInteger.get());//輸出12
//失敗案例
AtomicInteger atomicInteger1 = new AtomicInteger(10);
boolean result1 = atomicInteger1.compareAndSet(11, 12);
System.out.println(result1);//輸出false
System.out.println(atomicInteger1.get());//輸出10
           

六、AtomicBoolean的基本使用

隻有兩種值,為0和1,即真或假。

1、AtomicInteger的建立和get()方法

  • AtomicInteger的建立分為兩種:
    1. 無參的,預設值0
    2. 有參的,指定預設值
  • get():用于擷取目前值,該方法是不需要鎖的,因為他隻是去取值
AtomicBoolean bool = new AtomicBoolean();
System.out.println(bool.get());//輸出false
AtomicBoolean bool = new AtomicBoolean(true);
System.out.println(bool.get());//輸出true
           

2、set()

  • 該方法是不需要鎖的,因為set方法相當于初始化該變量了
AtomicBoolean bool = new AtomicBoolean();
bool.set(true);
System.out.println(bool.get());//輸出true
           

3、getAndSet(int)

  • 先取值,再設定值。
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.getAndSet(false);
System.out.println(result);//輸出true
System.out.println(bool.get());//輸出false
           

4、compareAndSet(boolean expect, boolean update)

  • 快速失敗政策,是用于判斷期望值是否與變量實際值相等,如果相等則将update指派給變量,否則失敗。
//成功案例
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.compareAndSet(true, false);
System.out.println(result);//輸出true
System.out.println(bool.get());//輸出false
//失敗案例
AtomicBoolean bool1 = new AtomicBoolean(true);
boolean result1 = bool1.compareAndSet(false, true);
System.out.println(result1);//輸出false
System.out.println(bool1.get());//輸出true
           

七、AtomicBoolean的作用

可以當做多線程中的開關flag,進而來代替sychronic這樣比較重的鎖

案例

  • 如果這個案例中使用基本資料類型的boolean作為标志位,則線程永遠不會停止。
package com.brycen.concurrency03.atomic;

import java.util.concurrent.atomic.AtomicBoolean;

public class AtomicBooleanFlag {
	private static AtomicBoolean flag = new AtomicBoolean(true);
	
	public static void main(String[] args) {
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				int i = 0;
				while (flag.get()) {
					i++;
				}
				System.out.println("i="+i);
			}
		}).start();
		
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		flag.set(false);
	}
}
           

輸出

八、AtomicLong的基本使用

他基本上與AtomicInteger的使用方法差不多,這邊就不示範了,簡單說一下他們的差別

AtomicLong與AtomicInteger的差別

  • AtomicLong比AtomicInteger多了一個VM_SUPPORTS_LONG_CAS的boolean變量,對應的值是VMSupportsCS8(),這個是由JVM調用的。

VMSupportsCS8()方法的作用

  • 因為long類型的資料是64位的,那麼在32位CPU中,就需要分高位和低位擷取一個long類型的資料,那麼就會無法保證原子性的操作。VMSupportsCS8()這個方法就是判斷該jvm、cpu是否支援long類型的lockless的CompareAndSet,如果支援,則VMSupportsCS8()方法就不會對資料總線進行加鎖了,如果不支援則需要對資料總線進行加鎖來保證原子性。

九、compareAndSwap算法(CAS)詳解

其實在原子類型中涉及到改變變量數值的操作都會進行CAS算法校驗。它是利用Unsafe發送彙編指令達到無鎖的狀态,其實他是有鎖的,隻不過他是CPU級别的,不是系統級别的,性能會非常高。它會對比我們的目前值是否和變量值一緻,如果一緻才會将最新值指派給變量,否則會快速失敗,然後進行循壞嘗試。

下面是Integer原子類型中改變數值時的部分源代碼,其中compareAndSet中就用到了CAS算法

for(;;){
	int current = get();
	int next = current + delta;
	if (compareAndSet(current, next))
		return current;
}
           

步驟分析

時序 線程1 線程2
int current = get(); current=1 current=1
int next = current + delta; next=2 next=2
if (compareAndSet(current, next)) 釋放CPU執行權 current==value:判斷變量的值value和目前值current是否一緻。一緻則将要next的值指派給value
if (compareAndSet(current, next)) current==value:判斷變量的值value和目前值current是否一緻。而線程1中的current值為1,value已經變為2,是以不等,則繼續循環,直至相等 釋放CPU執行權

十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象

1. 自定義一個異常

package com.brycen.concurrency03.atomic;

public class GetLockException extends Exception {
	
	public GetLockException() {
		super();
	}
	
	public GetLockException(String message) {
		super(message);
	}
}
           

2. 自定義一個利用compareAndSet算法的鎖

package com.brycen.concurrency03.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class CompareAndSetLock {
	private final AtomicInteger value = new AtomicInteger();
	private Thread currentThread = null;
	public void tryLock() throws GetLockException{
		boolean success = value.compareAndSet(0, 1);
		if (!success) {
//			不相等則抛出異常,表示有其他線程使用
			throw new GetLockException();
		}
		currentThread = Thread.currentThread();
	}
	
	public void unLock() {
		if (0 == value.get()) {
			return;
		}
//		確定解鎖的是拿到鎖的那個線程
		if (currentThread == Thread.currentThread()) {
			value.compareAndSet(1, 0);
		}
	}
}
           

3. 自定義一個利用compareAndSet算法的鎖

package com.brycen.concurrency03.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDetailTest2 {
	private static CompareAndSetLock lock = new CompareAndSetLock();

	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 5; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					// TODO Auto-generated method stub
					doSomeThing2();
				}
			}).start();
		}
	}
	//這個方法是利用synchronized實作的鎖,會使其他線程進入block狀态
	private static void doSomeThing() {
		synchronized (AtomicIntegerDetailTest2.class) {
			System.out.println(Thread.currentThread().getName()+": get the lock");
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	private static void doSomeThing2() {
		try {
			lock.tryLock();
			System.out.println(Thread.currentThread().getName()+": get the lock");
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (GetLockException e) {
			// TODO Auto-generated catch block
//			e.printStackTrace();
			System.out.println(Thread.currentThread().getName()+": do other things");
		}finally {
			lock.unLock();
		}
	}
}
           

輸出結果

Thread-0: get the lock
Thread-3: do other things
Thread-4: do other things
Thread-1: do other things
Thread-2: do other things