1. 程式人生 > >Java 7之多執行緒第5篇

Java 7之多執行緒第5篇

一道面試題:

假如有一個檔案可以允許多個人同時編輯,如果一個人在編輯完成後進行提交時,另外一個人已經對這個文件進行了修改,這時候就需要提醒下要提交的人,“文件已經修改,是否檢視?”

最為簡單的辦法就是:


其實原子類大體也是用到這樣的思想。

在java.util.concurrent包裡包含的主要就是一些與併發實現相關的類,首先來看一下最為基礎的原子類(java.util.concurrent.atomic)和和執行緒鎖(java.utl.concurrent.locks)。這一篇將著重講解一下原子類。

根據修改的資料型別,可以將java.util.concurrent.atomic包中的原子操作類可以分為4類。

1. 基本型別: AtomicInteger, AtomicLong, AtomicBoolean ;
2. 陣列型別: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
3. 引用型別: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
4. 物件的屬性修改型別: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

這些類存在的目的是對相應的資料進行原子操作。所謂原子操作,是指操作過程不會被中斷,保證資料操作是以原子方式進行的。

1、基本型別

拿Atomic舉例來說,呼叫類中相關的方法可以肯定,能夠返回一個唯一的數值。在這個類中有一個關鍵的變數定義如下:

private volatile int value;
這個私有的變數被volatile修飾,那麼volatile關鍵字的作用是什麼呢?
(1)可以使value在被某個執行緒修改後及時刷回到主記憶體中

(2)執行緒在獲取value值時,這個值必須要從主記憶體中取出

可以看到,其實被volatile修飾的原始型別類似於一個小小的同步塊,但是與同步塊比起來,由於沒有執行緒鎖這樣一個概念,所以在某些情況下還是得不到保證。例如要獲取一個唯一增長的序列時,還是會產生問題。

舉個例子:

public class UnsafeSequence {
    private volatile int value;
	public  int get() {
         return value++;
	}
}
在執行的時候,兩個執行緒在呼叫get()方法時很可能會得到相同的值。如何能保證一個唯一且增長的序列時,可能會給get()方法上鎖(加synchronized關鍵字或同步塊),這時候就不需要volatile關鍵字了,因為如果給get()方法上鎖,那麼同步塊本身會刷記憶體的。怎麼利用volatile來實現呢?

下面繼續來分析原始碼,發現這個類中提供了一些設定value值的方法,其中就包括對value值進行加1的操作,如下:

public final int getAndIncrement() {
        for (;;) {
            int current = get(); // 獲取value當前值
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
還有comareAndSet()方法的原始碼如下:
/**
     * Atomically sets the value to the given updated value
     * if the current value  == the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

當value值等於expect的時候,修改value的值為update,也就是給value值加1。可能有些人會問,為什麼要比較value和expect的值呢?這就是設計的巧妙之處。

試想一下,如果value值為2的時候,被兩個執行緒呼叫get()方法得到值後,其中執行緒1為value值加1後,呼叫compareAndSet()方法將value值修改為3並被刷回到記憶體中。執行緒2也要呼叫compareAndSet()方法時,這時的expect=2就和value=3的值不符合了,所以不會返回3,避免錯誤。

此時如何處理呢?在for(;;)死迴圈中重新作處理後,就可以得到正確的值4並且返回了。

下面我們來利用這個類得到 一個唯一且完全增長的序列,如下:

public class SafeSequence {
    private final AtomicInteger value=new AtomicInteger(0);
	public  int getSequence() {
         return value.getAndIncrement();
	}
}

AtomicLong類對long資料型別進行原子操作。在32位作業系統中,64位的long 和 double 變數由於會被JVM當作兩個分離的32位來進行操作,所以不具有原子性。而使用AtomicLong能讓long的操作保持原子性。除此之外,使用該類的方法,也會方便地得到唯一序列。

2、陣列型別

如上是對基本型別進行原子操作,而陣列型別是對陣列中的元素進行原子操作。只簡單的舉個例子,給陣列中指定索引處的某個元素加1後返回,方法如下:

    // Atomically decrements by one the element at index  i.
    public final int decrementAndGet(int i) {
        return addAndGet(i, -1);            // 對陣列索引i處儲存的值減去1
    }

    // Atomically adds the given value to the element at index  i.
    public final int addAndGet(int i, int delta) {
        long offset = checkedByteOffset(i); // 計算偏移量
        while (true) {
            int current = getRaw(offset);   // 獲取陣列中儲存的當前值
            int next = current + delta;     // 計算更新值
            if (compareAndSetRaw(offset, current, next)) // 對值進行原子性修改
                return next;
        }
    }
由於多個執行緒操作時,可能會存在安全隱患。例如,陣列0索引處儲存值為0,第一個執行緒的任務是對索引0處的值加1,並且等於第二個執行緒獲取到了原始的儲存值0,第二個執行緒任務是對0索引處的值加10,在獲取0索引值後馬上進行了修改,將值變為10。這時候第二個執行緒獲取到的結果就應該為11,而不是在原儲存值的基礎上加1。呼叫如上的方法可以避免多執行緒下的錯誤。

3、引用型別


AtomicReference是作用是對"物件"進行原子操作。測試用到方法的原始碼如下:

    // Atomically sets to the given value and returns the old value.
    public final V getAndSet(V newValue) {
        while (true) {
            V x = get();
            if (compareAndSet(x, newValue))
                return x;
        }
    }
編寫例子進行測試,如下:
public final static AtomicReference<String> ATOMIC_REFERENCE = new AtomicReference<String>("abc");  
      
    public static void main(String []args) {  
        for(int i = 0 ; i < 100 ; i++) {  
            final int num = i;  
            new Thread() {  
                public void run() {  
                    try {  
                        Thread.sleep(Math.abs((int)(Math.random() * 100)));  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                    if("abc"==ATOMIC_REFERENCE.getAndSet(new String("abc"))) {  
                        System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");  
                    }  
                }  
            }.start();  
        }  
    }  
只有一個執行緒得到了執行。

進一步擴充套件如上問題。試想,如果還有另外其他的執行緒在如上的執行緒執行完之前,又將new String("abc")修改為"abc",那麼如上的執行緒又會繼續修改,如下:

public class AtomicReferenceABATest {
	
	public final static AtomicReference <String> ATOMIC_REFERENCE = new AtomicReference<String>("abc");

	public static void main(String []args) {
		for(int i = 0 ; i < 100 ; i++) {
			final int num = i;
			new Thread() {
				public void run() {
					try {
						Thread.sleep(Math.abs((int)(Math.random() * 100)));
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2")) {
						System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");
					}
				}
			}.start();
		}
		new Thread() {
			public void run() {
				while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc"));
				System.out.println("已經改為原始值!");
			}
		}.start();
	}
}
執行後,發現進行了幾次修改。如果我們只想進行一次修改,這時候該怎麼辦?AtomicStampedReference解決這個問題:
public class AtomicStampedReferenceTest {
	
	public final static AtomicStampedReference <String> ATOMIC_REFERENCE = new AtomicStampedReference<String>("abc" , 0);
	
	public static void main(String []args) {
		for(int i = 0 ; i < 100 ; i++) {
			final int num = i;
			final int stamp = ATOMIC_REFERENCE.getStamp();
			new Thread() {
				public void run() {
					try {
						Thread.sleep(Math.abs((int)(Math.random() * 100)));
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2" , stamp , stamp + 1)) {
						System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");
					}
				}
			}.start();
		}
		new Thread() {
			public void run() {
				int stamp = ATOMIC_REFERENCE.getStamp();
				while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc" , stamp , stamp + 1));
				System.out.println("已經改回為原始值!");
			}
		}.start();
	}
}
可以看到,執行緒只進行了一次修改。


4、物件的屬性修改型別


AtomicIntegerFieldUpdater可以對指定"類的 'volatile int'型別的成員"進行原子更新。它是基於反射原理實現的。API的解釋如下:

A reflection-based utility that enables atomic updates to  designated volatile int  fields of designated classes.
This class is designed for use in atomic data structures in which several fields of the same node are independently subject to atomic
updates.

也就是保證volatile型別修改的原子性。測試程式如下:

public class LongFieldTest {
    
    public static void main(String[] args) {

        // 獲取Person的class物件
        Class cls = Person.class; 
        // 新建AtomicLongFieldUpdater物件,傳遞引數是“class物件”和“long型別在類中對應的名稱”
        AtomicLongFieldUpdater mAtoLong = AtomicLongFieldUpdater.newUpdater(cls, "id");
        Person person = new Person(12345678L);

        // 比較person的"id"屬性,如果id的值為12345678L,則設定為1000。
        mAtoLong.compareAndSet(person, 12345678L, 1000);
        System.out.println("id="+person.getId());
    }
}

class Person {
    volatile long id;
    public Person(long id) {
        this.id = id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public long getId() {
        return id;
    }
}
執行後的結果如下:
id=1000