java併發編程:Volatile關鍵字和Atomic類

  在學習併發編程的時候一定要了解Volatile關鍵字和Atomic類。現在讓我們來逐一瞭解。

1 Volatile關鍵字

  要想詳細瞭解Volatile關鍵字就必須瞭解一下線程之間的可見性,什麼是線程可見性呢?

1.1 可見性

  可見性是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程的修改結果,另一個線程馬上能看到。下面舉一個代碼列子來說明:

public class UseVolatitle extends Thread {
    private boolean isrunning = true;

    public void setIsrunning(boolean isrunning) {
        this.isrunning = isrunning;
    }
    @Override
    public void run() {
        System.out.println("開始啓動線程");
        while(isrunning) {
        }
        System.out.println("線程結束");
    }
    public static void main(String[] args) throws Exception {
        // 新建類實例
        UseVolatitle useVolatitle = new UseVolatitle();
        // 啓動線程方法
        useVolatitle.start();
        // 線程睡眠2000毫秒,也就是2分鐘。
        Thread.sleep(2000);
        // 把線程中isrunning值設置爲false
        useVolatitle.setIsrunning(false);
        System.out.println("主線線程將running設置爲false");
    }
}

  該程序的運行結果如下:

開始啓動線程
主線線程將running設置爲false

  雖然打印出了兩條語句但是控制檯沒有停止,死循環依然卡在哪裏。也就是主線程對相關線程isrunning的值改寫,沒有被相關線程可見,當試圖把變量加上volatile關鍵字。代碼如下:

public class UseVolatitle extends Thread {
    private volatile boolean isrunning = true;

    public void setIsrunning(boolean isrunning) {
        this.isrunning = isrunning;
    }

    @Override
    public void run() {
        System.out.println("開始啓動線程");
        while(isrunning) {
        }
        System.out.println("線程結束");
    }

    public static void main(String[] args) throws Exception {
        UseVolatitle useVolatitle = new UseVolatitle();
        useVolatitle.start();
        Thread.sleep(2000);

        useVolatitle.setIsrunning(false);
        System.out.println("主線線程將running設置爲false");
    }
}

  運行結果如下:

開始啓動線程
主線線程將running設置爲false
線程結束

  再次運行,雖然卡着幾秒鐘,但最終程序能夠正常終止。這就是volatile作用。

2 Atomic類

  Atomic類就是爲了實現原子性,主要的核心類有:1、AtomicInteger;2、AtomicLong;3、AtomicBoolean;4、AtomicIntegerArray;5、AtomicLongArray;6、AtomicReference。首先了解一下原子性。

2.1 原子性

  原子性操作的最小單位,例如轉賬業務:從農業銀行轉1塊錢到工商銀行。首先從用戶的農業銀行A帳號扣除1塊錢,然後再向用戶所在的工商銀行帳號B新增1塊錢。這兩個操作要麼同時成功,要麼同時失敗。下面舉一個代碼例子來深入瞭解Atomic類來操作原子性。

public class NoUseAtomic {
    static int count = 0;
    public int add() {
        count = count+10;
        return count;
    }
    public static void main(String[] args) {
        NoUseAtomic noUseAtomic = new NoUseAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(noUseAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  控制檯輸出結果如下:

10
20
30
40
......
960

  如果你仔細觀察,你會發現那個該死1000沒有出現,雖然這100個線程執行先後順序我們是不可預測的,但是我總是希望在某個線程的某個時刻輸出期待已久的1000,但現實總是令人悲傷的。現在讓我們來給我們的count變量替換Atomic類,使之能出現我們預期的結果。相關代碼如下:

public class UseAtomic {

    static AtomicInteger count = new AtomicInteger(0);

    public int add() {
        count.addAndGet(10);
        return count.get();
    }

    public static void main(String[] args) {
        UseAtomic noUseAtomic = new UseAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(noUseAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  運行上訴代碼,雖然輸出的先後順序也同樣是不可預期的,但是在衆多的輸出中,你會發現1000被打印出來。

注:(1)並非所有的Java基本數據類型都具有其原子類,比如並不存在AtomicChar,AtomicFloat和AtomicDouble等。
(2)雖然Atomic是原子性的,但是add方法併發原子性的。

2.2 方法原子性(synchronized)

  1.1講到add方法併發原子性,舉代碼例子說明如下:

public class WarmingInAtomic {
    static AtomicInteger count = new AtomicInteger(0);
    public int add() {
        count.addAndGet(1);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        return count.get();
    }

    public static void main(String[] args) {
        WarmingInAtomic warmingInAtomic = new WarmingInAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(warmingInAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  運行的部分結果如下:

109
118
127
136
145
154
163
172

  運行結果之間的最小間隔是1,這就是併發原子性造成的。如果方法上加上關鍵字synchronized,則保持原子性,相關代碼如下:

public class WarmingInAtomic {

    static AtomicInteger count = new AtomicInteger(0);

    public synchronized int add() {
        count.addAndGet(1);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        return count.get();
    }

    public static void main(String[] args) {
        WarmingInAtomic warmingInAtomic = new WarmingInAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(warmingInAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  輸出部分結果如下:

10
20
30
40
50
60
70
80
90
100

2.3 類原子性(AtomicReference)

  當我們在主線程利用其它線程修改類屬性,輸出一直讀的是主線程的類屬性。相關代碼如下:

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class UseAtomicRefrence1 {
    static Person person;
    public static void main(String[] args) throws InterruptedException {
        UseAtomicRefrence1 uar = new UseAtomicRefrence1();
        person = new Person();
        person.setName("root");
        person.setAge(18);
        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        Thread.sleep(1000);
        System.out.println("now value : " + person.toString());
    }

    static class Task1 implements Runnable {
        @Override
        public void run() {
            person.setAge(19);
            person.setName("admin");

            System.out.println("thread1 value : " + person.toString());
        }
    }

    static class Task2 implements Runnable {
        @Override
        public void run() {
            person.setAge(20);
            person.setName("super user");

            System.out.println("thread2 value : " + person.toString());
        }
    }
}

  這樣做是不能保證數據的一致性的,控制檯輸出如下:

thread1 value : Person{name='admin', age=19}
thread2 value : Person{name='super user', age=20}
now value : Person{name='super user', age=20}

  在類相應的對象初始化時用上AtomicReference屬性,相關代碼如下:

public class UseAtomicRefrence2 {
    //普通引用
    static Person person;

    //原子性引用
    static AtomicReference<Person> aPerson;

    public static void main(String[] args) throws InterruptedException {
        UseAtomicRefrence2 uar = new UseAtomicRefrence2();
        //new一個person類
        person = new Person();
        person.setName("root");
        person.setAge(18);
        //new一個AtomicReference原子性引用
        aPerson = new AtomicReference<>(person);
        System.out.println("啓動其他線程之前主線程的值:" + aPerson.get());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        Thread.sleep(1000);
        System.out.println("主線程當前值 : " + person.toString());
    }

    static class Task1 implements Runnable {
        @Override
        public void run() {
            Person expect = aPerson.get();
            Person update = new Person("admin", 19);
            //cas原子性操作
            aPerson.compareAndSet(expect, update);

            System.out.println("thread1 value : " + aPerson.get());
        }
    }

    static class Task2 implements Runnable {
        @Override
        public void run() {
            Person expect = aPerson.get();
            Person update = new Person("super user", 20);
            //cas原子性操作
            aPerson.compareAndSet(expect, update);

            System.out.println("thread2 value : " + aPerson.get());
        }
    }
}

  控制檯輸出如下:

啓動其他線程之前主線程的值:Person{name='root', age=18}
thread1 value : Person{name='admin', age=19}
thread2 value : Person{name='super user', age=20}
主線程當前值 : Person{name='root', age=18}

  從上面的控制題看出,主線程,thread1,thread2線程分別對person對象的設置能在各自的線程中打印出來,並且thread1,thread2對靜態變量的修改對主線程的對象屬性值沒有影響,保證了線程的安全。

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