以下幾種場景無需加鎖就能作到線程安全:java
1.不變對象web
2.線程封閉數據庫
3.棧封閉編程
4.ThreadLocal緩存
經典併發編程描述對象知足不變性有如下條件:安全
1.對象建立後狀態就再也不變化。bash
2.對象的全部域都是final類型。併發
3.建立對象期間,this引用沒有溢出。post
實際對於第2點描述不徹底準確:優化
1.只要成員變量是私有的,而且只提供只讀操做,就可能作到線程安全,並不必定須要final修飾,注意這裏說的是可能,緣由見第2點。
2.若是成員變量是個對象,而且外部可寫,那麼也不能保證線程安全,例如:
public class Apple {
public static void main(String[] args) {
Dictionary dictionary = new Dictionary();
Map<String, String> map = dictionary.getMap();
//這個操做後,致使下一步的操做結果和預期不符,預期不符就不是線程安全
map.clear();
System.out.println(dictionary.translate("蘋果"));
}
}
class Dictionary {
private final Map<String, String> map = new HashMap<String, String>();
public Dictionary() {
map.put("蘋果", "Apple");
map.put("橘子", "Orange");
}
public String translate(String cn) {
if (map.containsKey(cn)) {
return map.get(cn);
}
return "UNKONWN";
}
public Map<String, String> getMap() {
return map;
}
}
複製代碼
所以對不變對象的正確理解應該是:
1.對象建立後狀態再也不變化(全部成員變量再也不變化)
2.只有只讀操做。
3.任什麼時候候對象的成員都不會溢出(成員不被其餘外部對象進行寫操做),而不只僅只是在構建時。
另,一些書籍和培訓提到不變類應該用final修飾,以防止類被繼承後子類不安全,我的以爲子類和父類自己就不是一個對象,咱們說一個類是否線程安全說的是這個類自己,而不須要關心子類是否安全。
若是對象只在單線程中使用,不在多個線程中共享,這就是線程封閉。
例如web應用中獲取鏈接池中的數據庫鏈接訪問數據庫,每一個web請求是一個獨立線程,當一個請求獲取到一個數據庫鏈接後,不會再被其餘請求使用,直到數據庫鏈接關閉(回到鏈接池中)纔會被其餘請求使用。
對象只在局部代碼塊中使用,就是棧封閉的,例如:
public void print(Vector v) {
int size = v.size();
for (int i = 0; i < size; i++) {
System.out.println(v.get(i));
}
}
複製代碼
變量size是局部變量(棧封閉),Vector又是線程安全的容器,所以對於這個方法而言是線程安全的。
經過ThreadLocal存儲的對象只對當前線程可見,所以也是線程安全的。
有些容器線程安全指的是原子操做線程安全,並不是全部操做都安全,非線程安全的操做如:IF-AND-SET,容器迭代,例如:
public class VectorDemo {
public static void main(String[] args) {
Vector<String> tasks = new Vector<String>();
for (int i = 0; i < 10; i++) {
tasks.add("task" + i);
}
Thread worker1 = new Thread(new Worker(tasks));
Thread worker2 = new Thread(new Worker(tasks));
Thread worker3 = new Thread(new Worker(tasks));
worker1.start();
worker2.start();
worker3.start();
}
}
class Worker implements Runnable {
private Vector<String> tasks;
public Worker(Vector<String> tasks) {
this.tasks = tasks;
}
public void run() {
//以下操做非線程安全,多個線程同時執行,在判斷時可能都知足條件,但實際處理時可能已經再也不知足條件
while (tasks.size() > 0) {
//模擬業務處理
sleep(100);
//實際執行時,這裏可能已經不知足tasks.size() > 0
System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0));
}
}
private void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
輸出日誌:
Thread-0 task0
Thread-1 task2
Thread-2 task1
Thread-1 task3
Thread-2 task5
Thread-0 task4
Thread-0 task6
Thread-1 task8
Thread-2 task7
Thread-1 task9
Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
at java.util.Vector.remove(Vector.java:831)
at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46)
at java.lang.Thread.run(Thread.java:745)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
at java.util.Vector.remove(Vector.java:831)
at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46)
at java.lang.Thread.run(Thread.java:745)
複製代碼
能夠看到其中一個工做線程在tasks.remove(0)時,因爲集合中已經沒有數據而拋出異常。要作到線程安全則要對非原子操做加鎖,修改後的代碼以下:
public void run() {
//對非原子操做加鎖
synchronized (tasks) {
while (tasks.size() > 0) {
sleep(100);
System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0));
}
}
}
複製代碼
在上述例子中,即便用final修飾Vector也非線程安全,final不表明被修飾對象是屬於線程安全的不變對象。
volatile關鍵字修飾的對象只能保證可見性,這類變量不緩存在CPU的緩存中,這樣能保證若是A線程先修改了volatile變量的值,那麼B線程後讀取時就能看到最新值,而可見性不等於線程安全。
咱們說Vector是線程安全的,但上面的例子已經說明:並不是全部場景下Vector的操做都是線程安全的,但明明Vector又被公認爲是線程安全的,這怎麼解釋?
由此,咱們就能夠定義狹義線程安全和廣義線程安全:
1.狹義:對象的每個單個操做均線程安全
2.廣義:對象的每個單個操做和組合操做都線程安全
對於上面例子中的Vector要修改成廣義線程安全,就須要在remove操做中作二次判斷,若是容器中已經沒有對象,就返回null,方法簽名能夠修改成existsAndRemove,固然,爲了作到廣義線程安全,修改的方法還不只僅只有這一個。
本文描述了不加鎖狀況下線程安全的場景,以及容易誤解的線程安全場景,再到狹義線程安全和廣義線程安全,理解了這些,可讓咱們更清楚什麼時候該加鎖,什麼時候不須要加鎖,從而更有效的編寫線程安全代碼。
end.
相關閱讀:
Java併發編程(一)知識地圖
Java併發編程(二)原子性
Java併發編程(三)可見性
Java併發編程(四)有序性
Java併發編程(五)建立線程方式概覽
Java併發編程入門(六)synchronized用法
Java併發編程入門(七)輕鬆理解wait和notify以及使用場景
Java併發編程入門(八)線程生命週期
Java併發編程入門(九)死鎖和死鎖定位
Java併發編程入門(十)鎖優化
Java併發編程入門(十一)限流場景和Spring限流器實現
Java併發編程入門(十二)生產者和消費者模式-代碼模板
Java併發編程入門(十三)讀寫鎖和緩存模板
Java併發編程入門(十四)CountDownLatch應用場景
Java併發編程入門(十五)CyclicBarrier應用場景
Java併發編程入門(十六)秒懂線程池差異
Java併發編程入門(十七)一圖掌握線程經常使用類和接口
Java極客站點: javageektour.com/