什么是線程安全?
當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,並且在調用代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行為,那么就稱這個類是線程安全的。
怎么樣才能做到線程安全?
解決線程安全的方案:
1.基於JVM的鎖
無法解決分布式情況的問題
2.基於數據庫的鎖(分布式)
耗費資源
3.基於redis的鎖(分布式)
可能會出現死鎖
4.基於zookeeper的鎖(分布式)
最優級
實現好的並發是一件困難的事情,所以很多時候我們都想躲避並發。從下面幾點可以避免並發:
-
線程封閉
-
無狀態的類
- 讓類不可變
- volatile
- 加鎖和CAS
- 安全的發布
- ThreadLocal
線程封閉
什么是線程封閉?
就是把對象封裝到一個線程里,只有這一個線程能看到此對象。那么這個對象就算不是線程安全的也不會出現任何安全問題。
實現線程封閉有哪些方法?
ad-hoc 線程封閉
這是完全靠實現者控制的線程封閉,他的線程封閉完全靠實現者實現。
Ad-hoc 線程封閉非常脆弱,應該盡量避免使用。
棧封閉
棧封閉是我們編程當中遇到的最多的線程封閉。
什么是棧封閉呢?
簡單的說就是局部變量。
多個線程訪問一個方法,此方法中的局部變量都會被拷貝一份到線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現並發問題。所以能用局部變量就別用全局的變量,全局變量容易引起並發問題。
無狀態的類
沒有任何成員變量的類,就叫無狀態的類,這種類一定是線程安全的。
無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象.不能保存數據,是不變類。
如果這個類的方法參數中使用了對象,也是線程安全的嗎?比如:
讓類不可變
讓狀態不可變,兩種方式:
1,加 final 關鍵字,對於一個類,所有的成員變量應該是私有的,同樣的只要有可能,所有的成員變量應該加上 final 關鍵字,但是加上 final,要注意如果成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類是不可變的。
參見代碼
public class ImmutableClass { private final int a; private final UserVo user = new UserVo();//不安全 public int getA() { return a; } public UserVo getUser() { return user; } public ImmutableClass(int a) { this.a = a; } public static class User{ private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } } }
2、根本就不提供任何可供修改成員變量的地方,同時成員變量也不作為方法的返回值。
參見代碼
public class ImmutableClassToo { private final List<Integer> list = new ArrayList<Integer>(3); public ImmutableClassToo() { list.add(1); list.add(2); list.add(3); } public boolean isContain(int i){ return list.contains(i); } }
但是要注意,一旦類的成員變量中有對象,上述的 final 關鍵字保證不可變並不能保證類的安全性,為何?因為在多線程下,雖然對象的引用不可變,但是對象在堆上的實例是有可能被多個線程同時修改的,沒有正確處理的情況下,對象實例在堆中的數據是不可預知的。這就牽涉到了如何安全的發布對象這個問題。
volatile
加鎖和CAS
安全的發布
類中持有的成員變量,如果是基本類型,發布出去,並沒有關系,因為發布出去的其實是這個變量的一個副本.
參見代碼
/** * 演示基本類型的發布 */ public class SafePublish { private int i; public SafePublish() { i = 2; } public int getI() { return i; } public static void main(String[] args) { SafePublish safePublish = new SafePublish(); int j = safePublish.getI(); System.out.println("before j="+j); j = 3; System.out.println("after j="+j); System.out.println("getI = "+safePublish.getI()); } }
但是如果類中持有的成員變量是對象的引用,如果這個成員對象不是線程安全的,通過 get 等方法發布出去,會造成這個成員對象本身持有的數據在多線程下不正確的修改,從而造成整個類線程不安全的問題。
參見代碼
/** * 不安全的發布 */ public class UnSafePublish { private List<Integer> list = new ArrayList<Integer>(3); public UnSafePublish() { list.add(1); list.add(2); list.add(3); } public List getList() { return list; } public static void main(String[] args) { UnSafePublish unSafePublish = new UnSafePublish(); List<Integer> list = unSafePublish.getList(); System.out.println(list); list.add(4); System.out.println(list); System.out.println(unSafePublish.getList()); } }
這個 list 發布出去后,是可以被外部線程之間修改,那么在多個線程同時修改的情況下不安全問題是肯定存在的,怎么修正這個問題呢?
我們在發布這對象出去的時候,就應該用線程安全的方式包裝這個對象。
參見代碼
/** * 安全的發布 */ public class SafePublishToo { private List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>(3)); public SafePublishToo() { list.add(1); list.add(2); list.add(3); } public List getList() { return list; } public static void main(String[] args) { SafePublishToo safePublishToo = new SafePublishToo(); List<Integer> list = safePublishToo.getList(); System.out.println(list); list.add(4); System.out.println(list); System.out.println(safePublishToo.getList()); } }
我們將 list 用Collections.synchronizedList 進行包裝以后,無論多少線程使用這個 list,就都是線程安全的了。
private List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>(3));
對於我們自己使用或者聲明的類,JDK 自然沒有提供這種包裝類的辦法,但是我們可以仿造這種模式或者委托給線程安全的類,當然,對這種通過 get 等方法發布出去的對象,最根本的解決辦法還是應該在實現上就考慮到線程安全問題。
參見以上代碼 ↑↑↑
ThreadLocal
ThreadLocal 是實現線程封閉的最好方法。
ThreadLocal 內部維護了一個 Map,Map 的 key 是每個線程的名稱,而 Map 的值就是我們要封閉的對象。每個線程中的對象都對應着 Map 中一個值,也就是 ThreadLocal 利用 Map 實現了對象的線程封閉。