1. 程式人生 > >Java無鎖的實現——原子變數

Java無鎖的實現——原子變數

概述

對於併發控制來說,鎖是一種悲觀的策略。它總是假設每一次的臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果有多個執行緒同時訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待,所以說鎖會阻塞執行緒執行。 而無鎖採用的是一種樂觀的策略,它會假設對資源的訪問是沒有衝突的,既然沒有衝突,所以不用等待。遇到衝突,無鎖採用的策略是一種叫做CAS的技術來鑑別執行緒衝突。

CAS

CAS全稱為compile and swap,它包含三個引數(V,E,N)。V表示要跟新的變數,E表示預期值,N表示新值。僅當V值等於E值的時候,才會將V的值設為N,如果V值和E值不同,則說明有其他執行緒做了跟新,則當前執行緒什麼都不做。

原子變數

無鎖的執行緒安全整數:AtomicInteger

就具體實現來說,Atomic中儲存了一個核心欄位

private volatile int value;

這裡以具體demo的形式來給出用法,下面的幾個原子變數一致。

package nolock;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger();
    public static class AddThread implements Runnable{
        public void run(){
            for(int k = 0; k < 10000; k++){
                i.incrementAndGet();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Thread[] ts = new Thread[10];
        for(int k = 0;k < 10; k++){
            ts[k] = new Thread(new AddThread());
        }
        for(int k = 0; k < 10; k++)
            ts[k].start();
        for(int k = 0; k < 10; k++)
            ts[k].join();
        System.out.println(i);
    }
}

執行結果:

100000

無鎖的物件引用:AtomicReference

同理他是對物件的引用,但是這裡會有一個ABA問題。

如果A要訪問的物件值初始值為a,後來執行緒B改為b,再後來執行緒C改為a,此時就產生了髒讀了。所以但用這個AtomicReference解決不了問題,下面用這個例子來說明問題。

package nolock;

import java.util.concurrent.atomic.AtomicReference;

public class AtmoicReferenceDemo {
    static AtomicReference<Integer> money = new AtomicReference<Integer>();
    public static void main(String[] args){
        money.set(19);
        for(int i = 0; i < 3; i++){
            new Thread(){
                public void run(){
                    while(true){
                        while(true){
                            Integer m = money.get();
                            if(m < 20){
                                if(money.compareAndSet(m,m+20)){
                                    System.out.println("餘額小於20元,充值成功,餘額:" + money.get() + "元");
                                    break;
                                }
                            }else{
                                //無需充值
                                break;
                            }
                        }
                    }
                }
            }.start();

            new Thread(){
                public void run(){
                    for(int i = 0; i < 100; i++){
                        while (true){
                            Integer m = money.get();
                            if(m > 10){
                                System.out.println("大於10元");
                                if(money.compareAndSet(m,m-10)){
                                    System.out.println("成功消費10元,餘額:" + money.get());
                                    break;
                                }
                            }else{
                                System.out.println("沒有足夠的金額");
                                break;
                            }
                        }
                        try{
                            Thread.sleep(100);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
    }
}

這個例子的結果會無限迴圈下去,餘額值會一直浮動變化。

那麼用什麼來解決呢?

加一個時間戳stamp

AtomicStampedReference

帶有時間戳的物件引用:AtomicStampedReference

package nolock;

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    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("餘額小於20元,充值成功,餘額:" + 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){
                            System.out.println("大於10元");
                            if(money.compareAndSet(m,m-10,timestamp,timestamp+1)){
                                System.out.println("成功消費10元,餘額:" + money.getReference());
                                break;
                            }
                        }else{
                            System.out.println("沒有足夠餘額");
                            break;
                        }
                    }
                    try{
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

用這個方法餘額就只會贈予一次。

陣列也能無鎖:AtomicIntegerArray

package nolock;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {
    static AtomicIntegerArray arr = new AtomicIntegerArray(10);
    public static class AddThread implements Runnable{
        public void run(){
            for(int k = 0;k < 10000;k++){
                arr.getAndIncrement(k % arr.length());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Thread[] ts = new Thread[10];
        for(int k = 0;k < 10; k++){
            ts[k] = new Thread(new AddThread());
        }
        for(int k = 0;k < 10; k++)
            ts[k].start();
        for(int k = 0;k < 10; k++)
            ts[k].join();
        System.out.println(arr);
    }
}

讓普通變數也享受原子操作:AtomicIntegerFieldUpdater

package nolock;

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

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate{
        int id;
        volatile int score;//必須為volatile,並且不能private,也不能用static修飾
    }
    public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater
            = AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
    public static AtomicInteger allScore = new AtomicInteger(0);
    public static void main(String[] args)throws InterruptedException{
        final Candidate stu = new Candidate();
        Thread[] t = new Thread[10000];
        for(int i = 0; i < 10000; i++){
            t[i] = new Thread(){
                public void run(){
                    if(Math.random() > 0.4){
                        scoreUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }
        for(int i = 0; i < 10000; i++){
            t[i].join();
        }
        System.out.println("score = "+ stu.score);
        System.out.println("allScore = " + allScore);
    }
}

因為AtomicIntegerFieldUpdater保證了Candidate.score的執行緒安全,所以Candidate.score的值總是和allscore的值相等。

AtomicIntegerFieldUpdater的使用注意事項:

1.必須為volatile

2.不能private(因為Updater是使用反射得到這個變數的,如果變數不可見就會出錯)

3.不能用static修飾(因為Unsafe.objectFieldOffset()不支援靜態變數)

參考文獻:《Java高併發程式設計》