線程安全之原子性問題

原子操作

定義: 原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執
行其中的一部分

i++便不是原子操作

public class Counter {
    volatile int i = 0;

    public int getI() {
        return i;
    }

    public void add() {
        i++;
    }
}

測試代碼

public class Demo1_CounterTest {
    public static void main(String[] args) throws InterruptedException {
        final Counter ct = new Counter();

        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.getI());
    }
}

預計應該是打印i的值爲60000
而實際上
在這裏插入圖片描述
i++ 便存在競態條件

競態條件

當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件.換句話說,就是正確的結果要取決於運氣.
也就是說,如果兩個線程競爭同一資源時,如果對資源的訪問順序敏感, 就稱存在競態條件

解決辦法

1.同步鎖

示例代碼:

public class CounterLock {
    volatile int i = 0;

    Lock lock = new ReentrantLock();

    public void add() {
        lock.lock();
        i++;
        lock.unlock();
    }
}

或者

public class CounterSync {
    volatile int i = 0;

    public synchronized void add() {
        i++;
    }
}

2.CAS

CAS(Compare and swap) 機制

1.CAS 屬於硬件同步原語,處理器提供的內存操作指令,保證原子性。
2.CAS 操作需要兩個參數,一箇舊值和一個目標值,修改前先比較舊值是否改變,如果沒變,將新值賦給變量,否則不做改變。
3.JAVA中sun.misc.Unsafe類提供了CAS機制

Unsafe使用示例

public class CounterUnsafe {

    int i = 0;

    public int getI() {
        return i;
    }

    //Unsafe可以修改對象的屬性、可以修改數組等等
    private static Unsafe unsafe = null;

    //一個偏移量,代表了要修改的字段
    private static long valueOffset;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            // 暴力反射
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            //指定要修改的字段
            Field iFiled = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(iFiled);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add() {
        while (true) {
            if (unsafe.compareAndSwapInt(this, valueOffset, i, i + 1)) {
                return;
            }
        }
    }

}

CAS存在的問題

1.不能用於多個變量來實現原子操作
2.循環CAS自旋讓所有線程都處於高頻運行,爭搶CPU執行.如果長時間不成功,則會帶來很大的CPU資源消耗
3.ABA問題 (在多線程環境中,使用CAS時,如果一個線程對變量修改2次,第2次修改後的值和第1次修改前的值相同,那麼可能就會出現ABA問題
)

J.U.C包內的原子操作封裝類

常用的數據類型的原子操作封裝類:
AtomicBoolean: 原子更新布爾類型
AtomicInteger:原子更新整型
AtomicLong:原子更新長整型

如果將上面示例代碼的int i = 0 改爲原子操作封裝類

public class CounterAtomic {
    AtomicInteger at = new AtomicInteger(0);

    public void add(){
        // CAS操作進行累加
        at.getAndIncrement();
    }

    public int getValue(){
        return at.get();
    }
}

在這裏插入圖片描述
則可以打印出和預計值一樣的60000

數組的原子操作封裝類
AtomicIntegerArray:原子更新整型數組裏的元素。
AtomicLongArray:原子更新長整型數組裏的元素。
AtomicReferenceArray:原子更新引用類型數組裏的元素。

更新器
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新長整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段。

使用示例:

public class Demo_AtomicIntegerFieldUpdater {
    // 新建AtomicIntegerFieldUpdater對象,需要指明是哪個類中的哪個字段
    private static AtomicIntegerFieldUpdater<User> atom =
            AtomicIntegerFieldUpdater.newUpdater(User.class, "id");

    public static void main(String[] args) {
        User user = new User(100, 100);
        atom.addAndGet(user, 50);
        System.out.println("atom.addAndGet(user, 50)值變爲:" + user);
    }
}

class User {
    
    volatile int id;
    volatile int age;
    
    public User(int id, int age) {
        this.id = id;
        this.age = age;

    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                '}';
    }
}

原子更新引用類型
AtomicReference:原子更新引用類型。
AtomicStampedReference:原子更新帶有版本號的引用類型
AtomicMarkableReference:原子更新帶有標記位的引用類型。

原子計數器
DoubleAdder、 LongAdder:計數器增強版,高併發下性能更好
DoubleAccumulator、 LongAccumulator: 是計數器的增強版,可自定義累加規則

使用示例:

public class Demo_LongAccumulator {

    public static void main(String[] args) throws InterruptedException {

        LongAccumulator accumulator = new LongAccumulator(
                (x,y)->{
                    // 自定義計數規則
                    System.out.println("x:" + x);
                    System.out.println("y:" + y);
                    return  x+y;
                },
                0L);

        for (int i = 0; i < 3; i++) {
            accumulator.accumulate(1);
        }

        System.out.println(accumulator.get());
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章