使用Immutable對象解決線程安全

何爲Immutable對象?

簡單地說,如果一個對象實例不能被更改就是一個Immutable的對象,Java SDK提供的大量值對象,比如String等都是Immutable的對象。

如何使對象Immutable?

按照Effective Java的說明,需要滿足下面幾條規則:

  • 保證類不能被繼承 - 爲了避免其繼承的類進行mutable的操作
  • 移調所有setter/update等修改對象實例的操作
  • 保證所有的field是private和final的

爲什麼要採用Immutable對象?

在併發程序中,使用Immutable可以既保證線程安全性,跟併發鎖方式相比,它大大增強了併發時的效率。尤其當一個對象是值對象時,更應該考慮採用Immutable方式。

爲了說明,這裏先舉一個Mutable的非線程安全的例子。person應該是一個典型的值對象,但下面的例子沒有使他具備Immutable特性。

//non thread-safe
public class ImmutableDemo {
	static MutablePerson testM = new MutablePerson("joanieM", 14);
	
	public static void main(String[] args) {
		Thread t1 = new MutableTestThread(1);
		t1.start();
		Thread t2 = new MutableTestThread(2);
		t2.start();
	}
}

class MutablePerson {
	private int age; //Rule 1: all fields are private and final
	private String name;
	
	public MutablePerson(String name, int age) { //rule 2: a factory method pattern is adopted to create the object
		this.age = age;
		this.name = name;
	}

	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}

	public String toString() {
		return name +": "+age +"year(s) old";
	}
	
	public void updatePerson(String name, int age){
		this.name = name;
		this.age = age;
		System.out.println(this);
	}
}

class MutableTestThread extends Thread {
	final int MAX=10; 
	final int idx;

	public MutableTestThread(int idx) {
		this.idx = idx;
	}

	public void run() {
		for (int i = 0; i < MAX; i++) {
			ImmutableDemo.testM.updatePerson("joanieM"+idx, idx*MAX+i);
			
			try {
				Thread.sleep(20+i*2);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

運行結果:

joanieM1: 10year(s) old
joanieM2: 20year(s) old
joanieM2: 11year(s) old
joanieM2: 11year(s) old
joanieM2: 22year(s) old
joanieM2: 22year(s) old
joanieM2: 13year(s) old
joanieM2: 13year(s) old
joanieM2: 14year(s) old
joanieM2: 14year(s) old
joanieM1: 15year(s) old
joanieM2: 25year(s) old
joanieM1: 16year(s) old
joanieM1: 16year(s) old
joanieM2: 27year(s) old
joanieM1: 27year(s) old
joanieM1: 18year(s) old
joanieM2: 28year(s) old
joanieM2: 29year(s) old
joanieM1: 19year(s) old

結果中紅顏色標註的都是錯誤的結果。由於沒有采取任何保證線程安全性的操作,首先線程t1執行完updatePerson函數的this.age=age後被掛起,線程t2執行完updatePerson函數的this.name=name後被掛起,線程t1繼續執行,此時的name值爲t2執行的結果joanieM2,age則爲t1執行的結果,於是打印出了錯誤的值:joanieM2: 11year(s) old

下面的例子給出瞭如何使用Immutable來保證值對象的線程安全性的。

public class ImmutableDemo {
	//test is a shared thread-safe object. 
	static ImmutablePerson test = ImmutablePerson.getPerson("joanie", 14);
	
	public static void main(String[] args) {
		Thread t1 = new TestThread(1);
		t1.start();
		Thread t2 = new TestThread(2);
		t2.start();
	}
}

//a sample immutable class
//Rule 4: define class as final one
final class ImmutablePerson {
	private final int age; //Rule 1: all fields are private and final
	private final String name;
	
	private ImmutablePerson(String name, int age) { //rule 2: a factory method pattern is adopted to create the object
		this.age = age;
		this.name = name;
		System.out.println(this);
	}

	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}

	public String toString() {
		return name +": "+age +"year(s) old";
	}
	//Rule 3: no setters for value update. Create a new class instead
	public static ImmutablePerson getPerson(String name, int age) {
		return new ImmutablePerson(name, age);
	} 
}

class TestThread extends Thread {
	final int MAX=10; 
	final int idx;

	public TestThread(int idx) {
		this.idx = idx;
	}

	public void run() {
		for (int i = 0; i < MAX; i++) {
			ImmutableDemo.test = ImmutablePerson.getPerson("joanie"+idx, idx*MAX+i);
			try {
				Thread.sleep(20+i*2);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
運行結果:
joanie1: 10year(s) old
joanie2: 20year(s) old
joanie1: 11year(s) old
joanie2: 21year(s) old
joanie1: 12year(s) old
joanie2: 22year(s) old
joanie1: 13year(s) old
joanie2: 23year(s) old
joanie1: 14year(s) old
joanie2: 24year(s) old
joanie1: 15year(s) old
joanie2: 25year(s) old
joanie1: 16year(s) old
joanie2: 26year(s) old
joanie1: 17year(s) old
joanie2: 27year(s) old
joanie1: 18year(s) old
joanie2: 28year(s) old
joanie1: 19year(s) old
joanie2: 29year(s) old

使用Immutable不得不提到的一個問題是:由於其使用創建新對象來代替setter/update,勢必會造成過多垃圾回收的對象。因此,爲了性能的考慮,往往在爲某個對象提供其Immutable實現的同時,還需提供一個它的Mutable伴侶,就像StringBuffer相對於String,使用這些值對象時就需要綜合考慮了。

參考資料

1. Effective Java 第二版

2. http://www.artima.com/designtechniques/threadsafety5.html

3. Java concurrency in Practice

發佈了67 篇原創文章 · 獲贊 9 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章