關於線程安全的話題,足可以使用一本書來講解這些東西。<Java Concurrency in Practice> 就是講解這些的,在這裡
主要還是分析JVM中關於線程安全這塊的內容。
線程安全,有經驗的開發人員都聽過這個名詞,但是能否給到一個准確的定義,很難。
在 Java Concurrency in Practice裡面定義是:
當多個線程訪問一個對象時,如果不用考慮這些線程在運行時的環境下的調度和交替執行,
也不需要進行額外的同步,或者調用其他協作,這個情況下,線程就是安全的。
java中的線程安全可以定義5個級別:
1)不可改變。
也就是final修飾的詞。
public class ThreadSafeType { class ThreadContext{ public int id = -1; } public void doSomeThingBackground(ThreadContext context) { new Thread(){ @Override public void run(){ context.id++; } }.start(); } }
上面標紅的,會再android studio裡面提示錯誤:
從內部類中訪問本地變量context; 需要被聲明為最終類型。
也就是說在
public void doSomeThingBackground(final ThreadContext context)
要改成上面的類型,這就是線程安全的考慮。
2)絕對安全
絕對安全其實很難描述,比如Vector是安全的。但是在多線程的情況下,它也是不安全的。
3)相對安全
相對安全其實就是我們一般意義上的線程安全。
它需要保證對這個對象的單獨操作是安全的。但是對於特定的順序,需要一些方法保證線程安全。
4)線程兼容
這就是我們常見的情況,需要使用synchronized等手段來保證線程安全。
5)線程對立
比較極端的情況,就是無論怎麼加鎖,代碼無法並發運行。一種情況就是死鎖。
1)互斥同步
保持共享數據在同一時刻只被一個線程使用。
互斥是手段,同步是目的。
在java中最常見的就是synchronized方法。
synchronized標記的代碼,會生成monitorenter & monitorexit 2段代碼。
這是java編譯器自動生成的,不會有遺漏。使用其他鎖,lock & unlock成對出現,但是
開發者有時候會容易疏忽這個操作,尤其在catch代碼裡面忘記調用unlock,將是一個隱患。
java.util.concurrent 下面有不少同步的方法。ReentrantLock也是一個可以的方法,在1.5以前,性能
遠由於synchronized。但是在1.6, java還是把synchronized做了很大的提升。原因就是synchronized使用的
代碼已經遠遠大於ReentrantLock,並且引入ReentrantLock,可能會令需要開發者混淆。所以ReentrantLock可以認為是
一道開胃小菜而已。
2)非阻塞同步
互斥同步是一種阻塞同步,但是有些情況下,我們不需要互斥,只要能夠同步就可以。
java.util.concurrent.atomic.AtomicInteger
就是這樣一個自增的方法。
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }
源碼裡面沒有 互斥的操作,就是一直在循環,知道+1滿足就退出。
者就是非阻塞同步。
3)無同步
同步只是保證共享數據的手段,如果2個線程沒有共享數據,也就不需要同步。
1)自旋鎖
自旋鎖有時候會白白的耗用處理器的資源,但是沒有任何實際效果。
2)鎖消除
如果代碼不可能存在共享數據需要同步,編譯器就會把鎖拿掉
3)鎖粗化
原則上鎖的互斥模塊盡可能的小,但是如果對於同一對象,反復的lock & unlock 尤其是循環體中。
會帶來很大的性能損失。
參考:
《深入理解Java虛擬機》周志明
《Java Concurrency in Pratice》