非線程安全的ListHelper

Java併發編程實戰這本書裏提到了使用Collections.synchronizedList可以創建線程安全的容器,同時給了一個沒有正確使用該容器的反例ListHelper,這個反例看起來實現了同步,然而由於鎖不一致導致它並不是一個線程安全的類。代碼如下:

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = ! list.contains(x);
        if (absent)
            list.add(x);
        return absent;
    }
}

首先我們要知道這個類爲什麼不是線程安全的?很簡單這個類使用了兩個不同的鎖,putIfAbsent方法上使用的鎖是該方法的調用者ListHelper對象,然而list.contains(x)方法使用的卻不是這個鎖。查看contains的源碼,它使用的鎖是mutex。

        public boolean contains(Object o) {
            synchronized (mutex) {return c.contains(o);}
        }

顯然這個mutex鎖是SynchronizedCollection提供的。這就導致了鎖不一致的情況,也就會導致線程安全問題。

那麼我們如何證明ListHelper是非線程安全的呢?

如下是證明方案,分別啓動兩個線程,第一個線程循環100000次調用putIfAbsent方法添加數據。第二個線程同樣循環100000次添加,但使用了list作爲鎖對象來進行同步。

public class Test {
	public static void main(String[] args) throws Exception {
		ListHelper<Integer> helper = new ListHelper<>();
		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				Test.t1(helper);
			}
		});

		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				Test.t2(helper);
			}
		});

		thread1.start();
		thread2.start();
		thread1.join();
		thread2.join();
		System.out.println(helper.list.size());
	}

	private static void t1(ListHelper<Integer> helper) {
		for (int i = 0; i < 100000; i++)
			helper.putIfAbsent(i);
	}

	private static void t2(ListHelper<Integer> helper) {
		for (int i = 0; i < 100000; i++)
			synchronized (helper.list) { // correct way to synchronize
				if (!helper.list.contains(i))
					helper.list.add(i);
			}
	}
}

如果這段測試代碼打印的結果大於100000.那麼ListHelper就是非線程安全的,如下所示,確實非線程安全。

當然這個案例關注的問題是:單純來看putIfAbsent 這個方法,它本身一定是線程安全的,但由於該方法使用的鎖不正確,導致了putIfAbsent所屬的類卻不是線程安全的。如果你開啓100000個線程交替執行putIfAbsent方法,那麼始終輸出100000這個結果。

同時案例引發的思考是:如果引用外部線程安全的容器,那麼必須保證這個容器和類方法使用同一個鎖,這個類纔是線程安全的類。

所以,ListHelper要改造成線程安全的類,必須使用和list一致的鎖,即使用如下的同步代碼塊的方式:

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public boolean putIfAbsent(E x) {
        synchronized (list) {
            boolean absent = ! list.contains(x);
            if (absent)
                list.add(x);
            return absent;
        }
    }
}

 

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