在學習併發編程的時候一定要了解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對靜態變量的修改對主線程的對象屬性值沒有影響,保證了線程的安全。