什麼是線程安全性
在《Java 併發編程實戰》中,定義如下:
當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在調用代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
線程封閉
實現好的併發是一件困難的事情,所以很多時候我們都想躲避併發。避免併發最簡單的方法就是線程封閉。什麼是線程封閉呢?
就是把對象封裝到一個線程裏,只有這一個線程能看到此對象。那麼這個對象就算不是線程安全的也不會出現任何安全問題。實現線程封閉有哪些方法呢?
ad-hoc線程封閉
這是完全靠實現者控制的線程封閉,他的線程封閉完全靠實現者實現。 Ad-hoc 線程封閉非常脆弱,應該儘量避免使用。
棧封閉
棧封閉是我們編程當中遇到的最多的線程封閉。
什麼是棧封閉呢?
簡單的說 就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一份到 線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現併發問題。所以能用局部變量就別用全局的變量,全局變量容易引起併發問題。
無狀態的類
沒有任何成員變量的類,就叫無狀態的類,這種類一定是線程安全的。
參見代碼:
/**
1. 無狀態的類
*/
public class StatelessClass {
public int service(int a,int b){
return a+b;
}
public void serviceUser(UserVo user){
//do sth user
}
}
如果這個類的方法參數中使用了對象,也是線程安全的嗎?比如:
當然也是,爲何?因爲多線程下的使用,固然 user 這個對象的實例會不正 常,但是對於 StatelessClass 這個類的對象實例來說,它並不持有 UserVo 的對象 實例,它自己並不會有問題,有問題的是 UserVo 這個類,而非 StatelessClass 本身。
讓類不可變
讓狀態不可變,兩種方式:
- 加 final 關鍵字,對於一個類,所有的成員變量應該是私有的,同樣的只 要有可能,所有的成員變量應該加上 final 關鍵字,但是加上 final,要注意如果 成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類 是不可變的。
參見代碼:
/**
1. 類不可變
*/
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;
}
}
}
- 根本就不提供任何可供修改成員變量的地方,同時成員變量也不作爲方 法的返回值。
參見代碼:
/**
* 類不可變--事實不可變
*/
public class ImmutableClassToo {
private final List<Integer> list = new ArrayList<>(3);
public ImmutableClassToo() {
list.add(1);
list.add(2);
list.add(3);
}
public boolean isContain(int i){
return list.contains(i);
}
}
但是要注意,一旦類的成員變量中有對象,上述的 final 關鍵字保證不可變並不能保證類的安全性,爲何?
因爲在多線程下,雖然對象的引用不可變,但是 對象在堆上的實例是有可能被多個線程同時修改的,沒有正確處理的情況下,對 象實例在堆中的數據是不可預知的。這就牽涉到了如何安全的發佈對象這個問題。
volatile
並不能保證類的線程安全性,只能保證類的可見性,最適合一個線程寫,多個線程讀的情景。
加鎖和CAS
我們最常使用的保證線程安全的手段,使用 synchronized 關鍵字,使用顯式 鎖,使用各種原子變量,修改數據時使用 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<>(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 發佈出去後,是可以被外部線程之間修改,那麼在多個線程同時修 改的情況下不安全問題是肯定存在的,怎麼修正這個問題呢?我們在發佈這對象 出去的時候,就應該用線程安全的方式包裝這個對象。
參見代碼:
/**
1. 安全的發佈
*/
public class SafePublishToo {
private List<Integer> list
= Collections.synchronizedList(new ArrayList<>(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,就都是線 程安全的了。
對於我們自己使用或者聲明的類,JDK 自然沒有提供這種包裝類的辦法,但 是我們可以仿造這種模式或者委託給線程安全的類,當然,對這種通過 get 等方 法發佈出去的對象,最根本的解決辦法還是應該在實現上就考慮到線程安全問題
另外對容器的包裝
/**
* 仿Collections對容器的包裝,將內部成員對象進行線程安全包裝
*/
public class SoftPublicUser {
private final UserVo user;
public UserVo getUser() {
return user;
}
public SoftPublicUser(UserVo user) {
this.user = new SynUser(user);
}
private static class SynUser extends UserVo{
private final UserVo userVo;
private final Object lock = new Object();
public SynUser(UserVo userVo) {
this.userVo = userVo;
}
@Override
public int getAge() {
synchronized (lock){
return userVo.getAge();
}
}
@Override
public void setAge(int age) {
synchronized (lock){
userVo.setAge(age);
}
}
}
}
/**
* 類說明:委託給線程安全的類來做
*/
public class SafePublicFinalUser {
private final SynFinalUser user;
public SynFinalUser getUser() {
return user;
}
public SafePublicFinalUser(FinalUserVo user) {
this.user = new SynFinalUser(user);
}
public static class SynFinalUser{
private final FinalUserVo userVo;
private final Object lock = new Object();
public SynFinalUser(FinalUserVo userVo) {
this.userVo = userVo;
}
public int getAge() {
synchronized (lock){
return userVo.getAge();
}
}
public void setAge(int age) {
synchronized (lock){
userVo.setAge(age);
}
}
}
}
TheadLocal
ThreadLocal 是實現線程封閉的最好方法。 ThreadLocal 內部維護了一個 Map, Map 的 key 是每個線程的名稱,而 Map 的值就是我們要封閉的對象。每個線程 中的對象都對應着 Map 中一個值,也就是 ThreadLocal 利用 Map 實現了對象的線程封閉。
Servlet辨析
不是線程安全的類,爲什麼我們平時沒感覺到:
- 在需求上,很少有共享的需求
- 接收到了請求,返回應答的時候,一 般都是由一個線程來負責的。 但是隻要 Servlet 中有成員變量,一旦有多線程下的寫,就很容易產生線程安全問題。