本文Demo下載傳送門
寫在前面
本篇文章講的東西都是Android開源網絡框架NoHttp的核心點,當然線程、多線程、數據安全這是Java中就有的,為了運行快我們用一個Java項目來講解。
為什么要保證線程安全/數據同步
當多個子線程訪問同一塊數據的時候,由於非同步訪問,所以數據可能被同時修改,所以這時候數據不准確不安全。
現實生活中的案例
假如一個銀行帳號可以存在多張銀行卡,三個人去不同營業點同時往帳號存錢,假設帳號原來有100塊錢,現在三個人每人存錢100塊,我們最后的結果應該是100 + 3 * 100 = 400塊錢。但是由於多個人同時訪問數據,可能存在三個人同時存的時候都拿到原賬號有100,然后加上存的100塊再去修改數據,可能最后是200、300或者400。這種清情況下就需要鎖,當一個人操作的時候把原賬號鎖起來,不能讓另一個人操作。
案例(非線程安全)代碼實現:
1、程序入口,啟動三個線程在后台循環執行任務,添加100個任務到隊列:
/**
* 程序入口
*/
public void start() {
// 啟動三個線程
for (int i = 0; i < 3; i++) {
new MyTask(blockingQueue).start();
}
// 添加100個任務讓三個線程執行
for (int i = 0; i < 100; i++) {
Tasker tasker = Tasker.getInstance();
blockingQueue.add(tasker);
}
}
2、那我們再來看看MyTask這個線程是怎么回事,它是怎么執行Tasker這個任務的。
public class MyTask extends Thread {
...
@Override
public void run() {
while (true) {
try {
Tasker person = blockingQueue.take();
person.change();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分析一下上面的代碼,就是一直等待循環便利隊列,每拿到一個Tasker時去調用void change()方法讓Tasker在子線程中執行任務。
3、我們在來看看Tasker對象怎么執行,單例模式的對象,被重復添加到隊列中執行void change()方法:
public class Tasker implements Serializable, Comparable<Tasker> {
private static Integer value = 0;
public void change() {
value++;
System.out.println(value);
}
...
}
我們來分析一下上面的代碼,void change()每被調用一次,屬性value的值曾加1,理論上應該是0 1 2 3 4 5 6 7 8 9 10…這樣的數據被打印出來,最差的情況下也是1 3 4 6 5 2 8 7 9 10 12 11…這樣順序亂一下而已,但是我們運行起來看看:

我們發現了為什么會有3 4 3 3 這種重復數據出現呢?嗯對了,這就是文章開頭說的多個線程拿到的value字段都是2,然后各自+1后打印出來的結果都是3,如果應用到我們的銀行系統中,那這不是坑爹了麽,所以我們在多線程開發的事后就用到了鎖。
多線程保證數據的線程安全與數據同步
多線程開發中不可避免的要用到鎖,一段被加鎖的代碼被一個線程執行之前,線程要先拿到執行這段代碼的權限,在Java里邊就是拿到某個同步對象的鎖(一個對象只有一把鎖),如果這個時候同步對象的鎖被其他線程拿走了,這個線程就只能等了(線程阻塞在鎖池等待隊列中)。拿到權限(鎖)后,他就開始執行同步代碼,線程執行完同步代碼后馬上就把鎖還給同步對象,其他在鎖池中等待的某個線程就可以拿到鎖執行同步代碼了。這樣就保證了同步代碼在統一時刻只有一個線程在執行。Java中常用的鎖有synchronized和Lock兩種。
鎖的特點:每個對象只有一把鎖,不管是synchronized還是Lock它們鎖定的只能是某個具體對象,也就是說該對象必須是唯一的,才能被鎖起,不被多個線程同時使用。
synchronized的特點
同步鎖,當它鎖定的方法或者代碼塊發生異常的時候,它會在自動釋放鎖;但是如果被它鎖定的資源被線程競爭激烈的時候,它的表現就沒那么好了。
1、我們來看下下面這段代碼:
// 添加100個任務讓三個線程執行
for (int i = 0; i < 100; i++) {
Tasker tasker = new Tasker();
blockingQueue.add(tasker);
}
這段代碼是文章最開頭的一段,只是把Tasker.getInstance()改為了new Tasker();,我們現在給Tadker的void change()方法加上synchronized鎖:
/**
* 執行任務;synchronized鎖定方法。
*/
public synchronized void change() {
value++;
System.out.println(value);
}
我們再次執行后發現,艾瑪怎么還是有重復的數字打印呢,不是鎖起來了麽?但是細心的讀者注意到我們添加Tasker到隊列中的時候是每次都new Tasker();,這樣每次添加進去的任務都是一個新的對象,所以每個對象都有一個自己的鎖,一共3個線程,每個線程持有當前task出的對象的鎖,這必然不能產生同步的效果。換句話說,如果要對value同步,那么這些線程所持有的對象鎖應當是共享且唯一的!這里就驗證了上面講的鎖的特點了。那么正確的代碼應該是:
Tasker tasker = new Tasker();
for (int i = 0; i < 100; i++) {
blockingQueue.add(tasker);
}
或者給這個任務提供單例模式:
for (int i = 0; i < 100; i++) {
Tasker tasker = Tasker.getInstance();
blockingQueue.add(tasker);
}
這樣對象是唯一的,那么public synchronized void change()的鎖也是唯一的了。
2、難道我們要給每一個任務都要寫一個單例模式麽,我們每次改變對象的屬性豈不是把之前之前的對象屬性給改變了?所以我們使用synchronized還有一種方案:在執行任務的代碼塊放一個靜態對象,然后用synchronized加鎖。我們知道靜態對象不跟着對象的改變而改變而是一直在內存中存在,所以:
private static Object object = new Object();
public void change() {
synchronized (object) {
value++;
System.out.println(value);
}
}
這樣就能保證鎖對象的唯一性了,無論我們用new Tasker();和Tasker.getInstance();都不受影響。
我們知道,對於同步靜態方法,對象鎖就是該靜態放發所在的類的Class實例,由於在JVM中,所有被加載的類都有唯一的類對象,具體到本例,就是唯一的Tasker.class對象。不管我們創建了該類的多少實例,但是它的類實例仍然是一個。所以我們上面的代碼也可以改為:
public void change() {
synchronized (Tasker.class) {
value++;
System.out.println(value);
}
}
根據上面的經驗,我們的Tasker.getInstance();方法的具體應該就是:
private static Tasker tasker;
public static Tasker getInstance() {
synchronized (Tasker.class) {
if (tasker == null)
tasker = new Tasker();
return tasker;
}
}
3、 synchronized的代碼塊遇到異常后自動釋放鎖。我們上面提到synchronized遇到異常后自動釋放鎖,所以如果我們不能保證代碼塊是否會發生異常的情況下(當時是資源不緊張時)是可以使用synchronized,我們模擬一下:
public void change() {
synchronized (object) {
value++;
System.out.println(value);
}
if (value == 50)
throw new RuntimeException("");
}
上面代碼應該很清楚了,但value增加到50的時候,這個線程會發生異常,根據我們的推斷,執行50的這個線程發生崩潰,但是其他兩個線程應該還是正常執行的,我們來測試一下:

我們看到之前是三個數字一起打印,后來變成兩個線程一起打印了,很顯然一個線程崩潰了之后還有兩個線程在執行,說明object這個鎖被釋放了。
Lock
由於我們提到synchronized無法中斷一個正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。所以JSR 166小組花時間為我們開發了java.util.concurrent.lock框架,當Lock鎖定的方法或者代碼塊發生異常的時候,它不會自動釋放鎖;它擁有與synchronized相同的並發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)
Lock的實現類有哪些?我們在代碼中選中Lock,按下Ctrl + T,顯示出如下:

我們看到有一個讀出鎖ReadLock、一個寫入鎖WriteLock、一個重入鎖ReenTrantLock,我們這里主要說在多線程開發中用的最多的重入鎖ReenTrantLock。
廢話不多說了,其實代碼上來講和上面原來一樣的,我們看看怎么實現:
/** Lock模塊事例 **/
private static Lock lock = new ReentrantLock();
public void change() {
lock.lock();
{// 代碼塊
value++;
System.out.println(value);
}
lock.unlock();
}
我們看到使用也蠻簡單,而且擴展性更好。但是呢我們上面提到如果我們在這里發生了異常呢:
{// 代碼塊
value++;
System.out.println(value);
}
經測試,果然被鎖起來,所有線程都拿不到執行權限了,所以呢這里也給出一解決方案,哈哈也許你早就想到了,就是咱的try {} finally {}:
public void change() {
lock.lock();
try {
value++;
System.out.println(value);
if (value == 50)
throw new RuntimeException("");
} finally {
lock.unlock();
}
}
我們看到我們在上面的代碼中加了一個和synchronized一樣的異常,我們再次測試后發現,完全沒有發生異常啊是不是哈哈哈,這就是ReentrantLock,這位看的朋友你會用了嗎?
NoHttp 源碼及Demo托管在Github歡迎大家Star:https://github.com/yanzhenjie/NoHttp
對新手很有指導意義。。。。。。
