本篇文章講的東西都是Android開源網絡框架NoHttp的核心點,固然線程、多線程、數據安全這是Java中就有的,爲了運行快咱們用一個Java項目來說解。java
當多個子線程訪問同一塊數據的時候,因爲非同步訪問,因此數據可能被同時修改,因此這時候數據不許確不安全。git
假如一個銀行賬號能夠存在多張銀行卡,三我的去不一樣營業點同時往賬號存錢,假設賬號原來有100塊錢,如今三我的每人存錢100塊,咱們最後的結果應該是100 + 3 * 100 = 400塊錢。可是因爲多我的同時訪問數據,可能存在三我的同時存的時候都拿到原帳號有100,而後加上存的100塊再去修改數據,可能最後是200、300或者400。這種清狀況下就須要鎖,當一我的操做的時候把原帳號鎖起來,不能讓另外一我的操做。github
一、程序入口,啓動三個線程在後臺循環執行任務,添加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); } }
二、那咱們再來看看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
在子線程中執行任務。多線程
三、咱們在來看看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,若是應用到咱們的銀行系統中,那這不是坑爹了麼,因此咱們在多線程開發的過後就用到了鎖。ide
多線程開發中不可避免的要用到鎖,一段被加鎖的代碼被一個線程執行以前,線程要先拿到執行這段代碼的權限,在Java裏邊就是拿到某個同步對象的鎖(一個對象只有一把鎖),若是這個時候同步對象的鎖被其餘線程拿走了,這個線程就只能等了(線程阻塞在鎖池等待隊列中)。拿到權限(鎖)後,他就開始執行同步代碼,線程執行完同步代碼後立刻就把鎖還給同步對象,其餘在鎖池中等待的某個線程就能夠拿到鎖執行同步代碼了。這樣就保證了同步代碼在統一時刻只有一個線程在執行。Java中經常使用的鎖有synchronized和Lock兩種。
鎖的特色:每一個對象只有一把鎖,不論是synchronized仍是Lock它們鎖定的只能是某個具體對象,也就是說該對象必須是惟一的,才能被鎖起,不被多個線程同時使用。性能
同步鎖,當它鎖定的方法或者代碼塊發生異常的時候,它會在自動釋放鎖;可是若是被它鎖定的資源被線程競爭激烈的時候,它的表現就沒那麼好了。
一、咱們來看下下面這段代碼:
// 添加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()
的鎖也是惟一的了。
二、難道咱們要給每個任務都要寫一個單例模式麼,咱們每次改變對象的屬性豈不是把以前以前的對象屬性給改變了?因此咱們使用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; } }
三、 synchronized的代碼塊遇到異常後自動釋放鎖。咱們上面提到synchronized遇到異常後自動釋放鎖,因此若是咱們不能保證代碼塊是否會發生異常的狀況下(當時是資源不緊張時)是可使用synchronized,咱們模擬一下:
public void change() { synchronized (object) { value++; System.out.println(value); } if (value == 50) throw new RuntimeException(""); }
上面代碼應該很清楚了,但value增長到50的時候,這個線程會發生異常,根據咱們的推斷,執行50的這個線程發生崩潰,可是其餘兩個線程應該仍是正常執行的,咱們來測試一下:
咱們看到以前是三個數字一塊兒打印,後來變成兩個線程一塊兒打印了,很顯然一個線程崩潰了以後還有兩個線程在執行,說明object
這個鎖被釋放了。
因爲咱們提到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
對新手頗有指導意義。。。。。。