1. 程式人生 > >ThreadLocal和無鎖

ThreadLocal和無鎖

執行緒安全

執行緒安全,一般指的是某個物件,變數在進行多執行緒操作的時候,得到的結果與期望的結果一致。當我們使用執行緒去訪問一個物件時,由於物件內部的某些操作原因,而導致的執行緒的最終結果不符合期望,就是執行緒不安全,例如:ArrayList是一個執行緒不安全的容器,如果在多執行緒中使用ArrayList,程式有可能出現隱藏的錯誤。例如:

package com.dong.testThread;

import java.util.ArrayList;
/**
 * thread1,thread2執行緒向ArrayList容器中共新增200,0個容器,得到的結果應該是20000,但是執行結果卻不是20000:
 * 有三種結果:
 * ⑴丟擲異常:Thread-0" java.lang.ArrayIndexOutOfBoundsException:
 * ArrayList在擴容的過程中,內部一致性被破壞,由於沒有鎖的保護,另外一個執行緒訪問到了不一致的內部狀態,導致出現越界問題
 * ⑵不是期望的值,但也沒報錯
 * 由於多執行緒訪問衝突,是的儲存容器大小的變數被多執行緒不正常訪問,同時兩個執行緒同時對ArrayList中的同一個位置進行賦值導致的
 * ⑶正常結果: 20000
 * 
 * @author liuD
 *
 */
public class TestArrayList {
	static ArrayList<Integer> arrayList = new ArrayList<Integer>(66);
	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(new AddThread());
		Thread thread2 = new Thread(new AddThread());
		thread1.start();thread2.start();
		thread1.join();thread2.join();
		System.out.println(ar.size());
	}
	public static class AddThread implements Runnable{
		public void run() {
			for(int i = 0 ;i<10000;i++) {
				arrayList.add(i);
			}
		}
	}
}

ThreadLocal:執行緒的區域性變數,即只有當前執行緒可以訪問這個物件,因此每個執行緒都有自己的ThreadLocal變數,當執行緒中有執行緒不安全的物件,操作時,可使用ThreadLocal來修飾該例項,變數,讓每個執行緒都擁有一個該例項,變數,這樣就不會因為競爭同一個資源而導致的執行緒不安全。

ThreadLocal的實現原理:

ThreadLocal的set()方法和get()方法:

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  //獲取執行緒的ThreadLocalMap,可以理解為一個hashMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
getMap(t) -----》
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

t.threadLocals-----》
ThreadLocal.ThreadLocalMap threadLocals = null;

map.set(this, value) -----》
private void set(ThreadLocal<?> key, Object value) {  //key為ThreadLocal當前物件, value是需要的值
            Entry[] tab = table;  //Entry陣列物件,用於儲存map物件的陣列
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //利用key的雜湊和陣列長度,來確定當前ThreadLocal在entry[]中的下標
            for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();  //根據雜湊值獲取Entry物件
                if (k == key) {  //從entry陣列中獲取下標為i(i為當前ThreadLocal的雜湊值)的ThreadLocal,即當前的ThreadLocal,如果 k == 當前ThreadLocal;
                    e.value = value; //將物件的新值賦給當前ThreadLocal物件
                    return;
                }
                if (k == null) { //如果 == null ,則替換原來的物件;
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
createMap(t, value) -----》
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }




get()方法比較簡單:

   public T get() {
        Thread t = Thread.currentThread();  //獲取當前執行緒
        ThreadLocalMap map = getMap(t);  //獲取當前執行緒的ThreadLocalMap物件
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);  //如果不為null,則獲取Entry物件
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;   //獲取其ThreadLocal對應的值
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal變數都是在其內部,如果執行緒不終止,就不會被回收,所以如果想要回收,可以使用ThreadLocal.remove()方法將變數再其內部移除;

總結:ThreadLocal的實現原理是將例項儲存在一個數組中,陣列的元素是一個個entry物件,陣列的下標是ThreadLocal物件雜湊和陣列長度的與運算,因此,可以確保每個執行緒的ThreadLocal變數都是獨立的。

無鎖

前面我們介紹了CAS比較交換技術,可以實現不用鎖機制就能保證安全的併發,接下來介紹無鎖的一些類:

AtomicInteger: package java.util.concurrent.atomic;

private volatile int value;  //AtomicInteger的value值,被volatile修飾,執行緒之間對value的修改可知

public final int get() {//注意返回的是一個final值,證明返回值不可被修改,這也是防止多執行緒導致資料不一致的一種手段
        return value;
    }
public final void set(int newValue) { //同上
        value = newValue;
    }
public final int getAndSet(int newValue) {//設定新值返回舊值
        return U.getAndSetInt(this, VALUE, newValue);
    }
public final int getAndIncrement() {//讓value值增1,因為++不是執行緒安全的,同時AtomicInteger不使用鎖
        return U.getAndAddInt(this, VALUE, 1);
    }

//注意:在AtomicInteger中,有關的++,--,等賦值操作被方法替代,同時,value值為私有的,即只能使用公共方法獲取

無鎖的物件引用:AtomicReference

AtomicInteger是對整數的封裝,AtomicReference是對普通物件引用,保證你在修改物件引用時的執行緒安全。

AtomicReference的一個缺點是:使用比較交換技術,執行緒來判斷一個物件是否可以被修改,如果當前值和期望值一致,則執行緒可以寫入新值,如果當前值和期望值不一致,則證明有其他執行緒修改了值,故不寫入新值,但是如果其他執行緒修改多次,又恰好修改到當前值的期望值,這個時候,也符合寫入新值的條件,但是物件是否修改我們無法界定,對於一般問題,這個無所謂,例如我們做運算,不影響,但是對於業務,我們需要提防。因為做運算我們關注的是結果,但是對於業務,比較次數也有可能考慮在內。

Java引入AtomicStampedReference,即帶時間戳的物件引用,即不僅更新值,還得更新時間 戳,只有都滿足,才會修改資料;

普通變數也可以原子操作:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater分別可以對int,long,普通物件進行CAS修改,

什麼時候使用: 當代碼都完成差不多,其中存在普通變數導致的執行緒不安全操作,為了做到最少的修改,可以使用工具類AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater來實現原子操作。

package com.dong.testThread;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
 * 使用AtomicIntegerFieldUpdater 來說普通變數具有原子操作
 * 
 * @author liuD
 */
public class TestAtomicIntegerFieldUpdater {
	
	public static void main(String[] args) {
		AtomicIntegerFieldUpdater<att>  aifu =
				AtomicIntegerFieldUpdater.newUpdater(att.class,"vote");
		final att updateobj = new att();
		int newvalue= aifu.incrementAndGet(updateobj);
		System.out.println(newvalue);
	}
}
class att{
	 volatile int vote = 1 ;  //注意這裡不可以是static修飾;
}

 其他操作同理

最後:內容來自《Java高併發程式設計》   作者葛一鳴 郭超  由衷感謝作者為我們提供書籍內容;