并發與高并發系列之線程安全性之原子性
本文轉載自微信公眾號「安琪拉的博客」,作者安琪拉。轉載本文請聯系安琪拉的博客公眾號。
大家好,我是安琪拉,這是并發編程的第五集,完整大綱如下:
面試官:你好,你先自我介紹一下吧。
安琪拉:面試官你好,我是草叢三婊,最強中單,火球擁有者、不焚者,安琪拉,這是我的簡歷,請過目。
面試官:聽前一個面試官說你Java并發這塊掌握的不錯,我們深入的交流一下;
安琪拉:好好好,可以交流的深入一點
面試官:什么是線程安全性?
安琪拉:這個問題第一次被問,但是個好問題。
當多個線程訪問某個類時,不管運行環境采用何種調度方式或者這些進程將如何交替執行,并且在主調代碼中不需要任何額外的協同或者同步,這個類都能表現出正確的行為,那么這個類是線程安全的。
面試官:線程安全性有哪三大特點? 或者說線程不安全是由于什么引起的?
安琪拉:【太老套了吧,能不能來點新的】
線程不安全的原因:
當前的一個操作可能不是原子的,執行過程中會被打斷,其他線程有能力修改共享變量的值,同時存在線程修改的值不是立即對其他線程可見的,因為線程有自己的執行空間,另外一點就是存在程序可能存在亂序執行的情況,單線程沒問題,但是多個線程同時執行,線程共享的數據會出現錯亂,以上說的自己問題歸納出線程安全需要保證的三個特性:
- 原子性
提供互斥訪問、同一時刻只能有一個線程在操作
- 可見性
一個線程對主內存的修改可以及時地被其他線程看到
- 有序性
有序性是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。(你可能會想難道還有不一致的,是的,因為存在指令重排序,為什么會有指令重排,因為性能優化的需要,比如把多次訪問主存合并到一起執行比計算和訪問主存交替訪問更高效),重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
面試官:那你用過java.util.concurrent.atomic包下原子性相關的類嗎?
安琪拉:用過的,Java提供了很多AtomicXXX相關的原子類,如下圖所示:
面試官:能舉個例子說明下用法嗎?
安琪拉:比如存在并發,計數的場景,以netty為例,它的線程池工廠類如下:
nextId就是 AtomicInteger類型的。每次創建線程給線程命名的時候, 代碼如下:
- public Thread newThread(Runnable r) {
- Thread t = this.newThread(new DefaultThreadFactory.DefaultRunnableDecorator(r), this.prefix + this.nextId.incrementAndGet());
- try {
- if (t.isDaemon()) {
- if (!this.daemon) {
- t.setDaemon(false);
- }
- } else if (this.daemon) {
- t.setDaemon(true);
- }
- if (t.getPriority() != this.priority) {
- t.setPriority(this.priority);
- }
- } catch (Exception var4) {
- }
- return t;
- }
通過 incrementAndGet 實現原子性的 +1。
面試官:如果不用AtomicInteger,就用普通的int 會有什么后果?
安琪拉:首先我們知道 +1 操作不是原子性的,可以分成這么幾條指令:取數指令,將數據壓入操作數棧,執行+1操作,賦值。
關于指令這塊,扔個藍。我們編譯一段Java code 看一下。
代碼和字節碼指令分別為:
- public static int add(int a,int b){
- int c = 0;
- c = a + b;
- return c;
- }
指令,對應的操作解釋也有,如下:
- public static int add(int, int);
- Code:
- 0: iconst_0 //初始化常量0壓入操作數棧頂
- 1: istore_2 //彈出操作數棧棧頂元素,保存到局部變量表第2個位置
- 2: iload_0 //復制a變量的值入棧
- 3: iload_1 //復制b變量的值入棧
- 4: iadd //執行加操作,相加結果放在棧頂
- 5: istore_2 //彈出操作數棧棧頂元素,保存到局部變量表第2個位置
- 6: iload_2 //復制局部變量表第2個位置的值入棧
- 7: ireturn //彈棧,返回結果
寫這么多就是為了讓大家明白 a += 1 這種操作它不是原子的,是有多條指令組成,真的不容易,快給我點個贊,好心人的藍buff
面試官:那能跟我講下Atomic 的實現原理嗎?
安琪拉:【要開始卷了,到安琪拉最愛的源碼環節】
- /**
- * Atomically increments by one the current value.
- *
- * @return the updated value
- */
- public final int incrementAndGet() {
- return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
- }
代碼很短,注釋就一句話,原子性的增加當前值。
繼續下探:
- public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5;
- do {
- var5 = this.getIntVolatile(var1, var2);
- } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
- return var5;
- }
入參是三個值:var1、var2、var4 ,我們先看下這三個值分別是什么?
Val1:this ,也就是AtomicInteger 對象nextId
Val2:valueOffset 看下代碼,我另外畫了個圖,我們知道一個對象存儲空間由對象頭和成員變量組成的,那valueOffset 就是成員變量value 在AtomicInteger 對象中的偏移量。
初學者可能會問,函數放在哪呢?函數都放在方法區,因為是屬于類的,不是對象私有的。
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
- static {
- try {
- valueOffset = unsafe.objectFieldOffset
- (AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception ex) { throw new Error(ex); }
- }
- private volatile int value;
Val4:1
那開始詳細解釋下,下面這段代碼:
compareAndSwapInt 方法:比較val1(AtomicInteger對象)的var2(valueOffset偏移量)的值與var5(原始值)是否相等,如果相等,讓值更新成var5(原始值) + val4(1)
- //val1: nextId val2: valueOffset val4: 1
- public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5; //臨時變量
- do {
- var5 = this.getIntVolatile(var1, var2); //這是個native方法,獲取value的值
- //比較val1(AtomicInteger對象)的var2(valueOffset偏移量)的值與var5(原始值)是否相等,如果相等,讓值更新成var5(原始值) + val4(1)
- } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
- return var5;
- }
- public native int getIntVolatile(Object var1, long var2);
compareAndSwapInt 就是Java中非常重要,也是非常出名的CAS操作,比較并交換,并發底層框架用到的地方很多。
compareAndSwapInt 會返回CAS支持狀態,如果執行失敗,會循環執行,直到成功。
失敗的原因一般是同時有別的線程修改了這個變量的值,所以比較的時候不相等,下次執行會獲取最新值執行CAS。
。。。。嚶嚶嚶,打字好累啊,先寫到這,要去吃自助餐了,明天再寫可見性和有序性。