2009年5月15日 星期五

Practical Java Programming Language Guide-第五章 多緒

章節開頭就來一句

如果只追求容易和速度,工作就失卻了恆久的穩固或毫釐無誤所帶來的美感
For easy and speed in doing a thing do not give the work lasting solidity or exactness of beauty.

這提醒了我們,在追求效率的同時,不該忽略其他面的事物,比如說捨棄設計而帶來的設計債(Desing Dept)
還記得基峯出過一套遊戲程式設計精華系列,裡面就有強調:不要寫死任何程式

case 46:面對instance函式,synchronized鎖定的是物件(Object)而非函式(Method)或程式碼(Code)
有關同步行程方面知識有念過恐龍本(作業系統)的應該都非常清楚了,我就簡單帶過
synchronized可以替我們實現鎖(lock)的機制,互斥鎖確保了程式的正確性
書中提了一段程式碼

class Test{
public synchronized void method1(){}//修飾method

public void method2(){
synchronized(this){}修飾object reference
}
public void method3(Object obj){
synchronized(obj){}修飾object reference
}
}

method1跟method2都是鎖定同一個物件,都是對this進行同步控制,在這邊的lock就是this
而method3則是把參數obj當作lock,要注意一點,lock是指reference的memory本身而非變數
這在另一本『Effective Java Programming Language Guide』有提出相關議題


case 47:弄清楚synchronized static函式與synchronized instance函式之間的差異
這邊探討synchronized在static跟non-static函式上有哪些不同



class Turn{
public synchronized static void f1(){
while(true)
{
System.out.println("f1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void f2(){
synchronized (Turn.class) {
while(true)
{
System.out.println("f2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
}

在這邊f1跟f2是爭取同一個lock,都是想要取得Turn這個class
主程式

Thread t1=new Thread(new Runnable(){

@Override
public void run() {
Turn.f1();

}

});
Thread t2=new Thread(new Runnable(){

@Override
public void run() {
Turn t=new Turn();
t.f2();

}

});
t1.start();
t2.start();

印出的結果是

f1
f1
f1
...

但是如果將其中一個程式改成聽取instance的lock如下


class Turn{
public synchronized static void f1(){
while(true)
{
System.out.println("f1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void f2(){
synchronized (this) {
while(true)
{
System.out.println("f2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
}

結果就會變成

f1
f2
f2
f1
f2
f1
f1
f2
...

在第一個case裡面t2監聽的跟t1一樣都是Turn這個Class
但是到了第二個case,t2變成監聽Turn t=new Turn()這個lock
在類別與實體的同步控制,必須要格外小心
修正方法

class Turn{
byte[] lock=new lock[0];
public static void f1(Turn t)
{
synchronized (t.lock){}
}
public void f2()
{
synchronized (lock){}
}
}

讓要同步控制的函式去監聽同一個物件,置於為何用一個0byte的陣列
是因為這樣最節省空間並且經濟實惠
創造一個byte為0的array並不需要呼叫建構式(比方說用Object lock=new Object();)
而且byte arrays在JVM比int arrays有更緊湊的表述

case 48:以private的accessor取代/public/protected
當我們用synchronized進行同步控制的時候,必須保護好lock,必面被惡意攻擊造成程式碼漏洞


class Foo{
byte[] lock=new lock[0];
void f(){
synchronized(lock){...}
}

}

類似上面的程式碼,如果用戶端不安好心
使用以下攻擊手法

Foo foo=new Foo();
foo.f();//沒事
foo.lock=null;
foo.f();//丟出NullPointException例外

諸如此類,對於lock應該用privae修飾好好保護


class Foo{
private byte[] lock=new lock[0];
void f(){
synchronized(lock){...}
}

}


case 49:避免無謂的同步控制
這個case指出只有在真正需要的時候使用同步控制
一方面提升效能一方面避免死(deadlock)

case 50:取用共享變數時使用synchronized或volatile
Java允許每個執行緒都有變數的私有專屬複本(private working copy),但是有時候會希望多個執行緒之間共用變數,為了保持交易的不可分割(atomic)必須使用synchronized或volatile
比較synchronized和volatile




技術優點缺點
synchronized存取lock時進行私有專屬複本與主記憶體正本一致化消除了並行(con-currency)的可能性
volatile允許並行(con-currency)每次取用變數就進行一致化

這邊所謂並行,是只兩個執行緒可否同時執行,因為使用synchronized,另一個執行緒就非得等到變數被釋放後才能執行,但是volatile沒有這方面的困擾,他會維護變數的一致性而又步去鎖定他
舉個例子

class VolTest extends Thread{
static int t=0;

@Override
public void run() {
while(true)
{
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

主程式

VolTest f1=new VolTest();
VolTest f2=new VolTest();
f1.start();
f2.start();
...
//印出結果
1
2
3
3
4
4
5
6
8
7//直接跳過8
9
...

變數的值不一致,但是我只有小小修改一下
使用synchronized

class VolTest extends Thread{
static Integer t=0;

@Override
public void run() {
while(true)
{
synchronized(t){
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
...
//印出結果
1
2
3
4
5
6
7
8
...


變數是一致了,但是也造成了執行緒執行時候的阻塞

使用volatile

class VolTest extends Thread{
volatile static int t=0;

@Override
public void run() {
while(true)
{
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
...
//印出結果
1
2
3
4
5
5
6
7
8
8
9
9
10
11
...

變數一致了,而且也沒有被鎖定住,執行緒沒有阻塞
這邊沒有說誰比較好,規則是死的人是活的synchronized可以讓行程序列化而不並行
但是他只能鎖定在Heap上的記憶體也就是一個物件(上面例子必須用Integer)
而volatile雖然沒辦法讓行程序列化,但是他可以用於原生變數(上面例子可使用int)

case 51:在單一操作中(single operation)鎖定所有用到的物件
簡單來說,就是希望只要在執行緒用到的物件,就該把他鎖起來

int sum(int[] a1,int [] a2)
{
synchronized(a1){
synchronized(a2){//行程中會用到a1跟a2相加,兩個都該鎖起來
//do something
}
}
}


case 52:以固定而且全域性順序取得多個lock避免死結(deadlock)
學過OS應該都知道,當A等B而B也在等A的時候,就會造成死結
而這個case就是說要避免這種可能
舉個例子

int sum(int[] a1,int [] a2)
{
synchronized(a1){
synchronized(a2){//行程中會用到a1跟a2相加,兩個都該鎖起來
//do something
}
}
}

如果主程式這樣寫

sum(a1,a2);//thread1
...
sum(a2,a1);//thread2

這類程式碼,因為thread1是先取得a1在去取a2,而thread2正好相反先去取得a2才去取a1
假設thread1先取得a1,但是執行權被thread2拿去而取得a2,但是a1被thread1拿走了他就阻塞
而執行回到thread1,他想取a2但是a2被thread2拿走了,此時就造成了死結(deadlock)
thread1擁有a1等thread2的a2,而thread2擁有a2等thread1的a1 誰也動不了
一個簡單的解決方法就是把lock物件化

ArrayLock{
static int num=0;
int order;
int[] arr;
ArrayLock(int[] a){
arr=a;
synchronized(ArrayLock.class){//num共用,必須保護
num++;
order=num;
}
}
}

用類似這種方法替lock排序
這樣程式碼只要改程如下就可以避免死結

int sum(ArrayLock a1,ArrayLock a2)
{
ArrayLock first,second;

//先將lock排序
if(a1.order>a2.order)
{ first=a1;second=a2;}
else
{
first=a2;second=a1;
}

//如此一來鎖定就不成問題
synchronized(first){
synchronized(second){
//do something
}
}
}


case 53:優先使用notifyAll()而非notofy()
notofy()會喚醒現在在等lock的執行緒,但是一次只會喚醒一個,而不確定是哪一個
如果是雙執行緒還可以用,但是多執行緒還是得用notifyAll()

case 54:針對notifyAll和wait使用旋鎖(spin lock)
case 55:使用notifyAll和wait取代輪詢迴圈(polling loops)
上面兩個case請參照
Spin lock跟polling loop

case 56:不要對locked object(上鎖物件)之object reference重新賦值
簡單來說,就是不要做下面的動作

...
synchronized(lock){
lock=new Lock();//不應該
}

這會造成lock無限產生而無法達到同步化的效果,因為lock的對象是instance記憶體
從新參照會讓lock變成孤兒

case 57:不要呼叫stop()或suspend()
這兩個是不安全的函式,目前被java列為不建議使用函式
主要是因為怕stop之後仍有某些東西被等待釋放的執行緒把持
比如有個執行緒正在將資料寫入共享記憶體緩衝區(shared memory buffer),如果此時該執行緒被stop,無法得知他是否已經write完成,那個緩衝區可能將不在處於有效狀態

case 58:透過執行緒之間的協作來中止執行緒
這個case算是替上一個case提出解法
間單來說就是用一個變數控制執行緒的存活


class Work extends Thread{
private volatile boolean stop=false;//使用volatile確保run可以看件stop的變化
...
public void stopWork(){//一個公用函式,提供外界手動停止
stop=true;
}

public void run(){

if(!stop){
//do work
}
}
}


如上,因為stop是由另一個執行緒控制,必須使用前面的機制volatile或是synchronized確保run可以即時正確的知道stop現在的狀態

沒有留言: