Java必知必會之(四)--多線程全揭祕(下)

本文旨在用最通俗的語言講述最枯燥的基本知識。java

全文提綱:
1.線程是什麼?(上)
2.線程和進程的區別和聯繫(上)
3.建立多線程的方法(上)
4.線程的生命週期(上)
5.線程的控制(上)
6.線程同步(下)
7.線程池(下)
8.線程通訊(下)
9.線程安全(下)
10.ThreadLocal的基本用法(下)面試


上集已經講述了Java線程的一些基本概念,本文接下來說述的是Java的一些高級應用。算法

6.線程同步

一開始接觸「線程同步」這個概念能夠有點難以理解,咱們來舉個栗子:數據庫

爸爸開了一張銀行卡存進去10000塊錢,是留給在山東讀大學的哥哥和在河南老家讀高中的妹妹用的。哥哥前天取了2000,變成8000,妹妹昨天取了1000,剩餘7000,今天他們同時到銀行同時取錢,哥哥打開時ATM發現有7000餘額,妹妹打開時也發現是7000餘額,他們同時按下肯定取1000錢,當他們取完錢以後在查看餘額發現只有5000塊錢,都在想我只取了1000啊怎麼扣了我2000呢?
這就是生活中的「同步」問題了。
咱們把思惟轉入到這個ATM的後臺程序,幸虧後臺程序對取錢的操做作了同步動做的監聽器,能在多線程同時操做的過程當中把取錢的動做給鎖定起來,若是程序沒有處理同步問題,那兩邊的ATM的算術都是:7000-1000,結果是剩餘6000.這樣子,銀行對帳就會出錯了。編程

所以可見,併發編程不合理使用也會帶來一些弊端,而針對多線程併發的問題,Java引入了同步監視器來解決問題:當線程要執行同步代碼塊/方法以前,必須先得到對同步監視器的鎖定。
Java中鎖用在的地方有:數組

  1. 代碼塊
  2. 方法(構造器、成員變量除外)

1.代碼塊同步

語法:緩存

1synchronized (obj) {
2//同步內容(好比取錢的操做)    
3}
複製代碼

其中obj就是同步監視器,也就是說任何線程要進入執行該代碼塊以前,首先得到對obj的鎖定,得到以後,其它線程就沒法獲取它,修改它,直到當前線程釋放位置。
好比:爸爸的銀行卡帳戶安全

1public BankCardAccount bankAccount;
2synchronized (bankAccount) {
3//對bankAccount的扣錢動做    
4}
複製代碼

當哥哥和妹妹同時取錢時,就如同兩個線程在執行,當其中一個線程獲取到對bankAccount的鎖定時,另外一個線程必須等待當前線程用完以後釋放bankAccount的鎖定,才能夠得到而且修改之多線程

2.方法同步

語法:併發

1修飾符 synchronized 返回值 方法名(形參列表){
2}
複製代碼

方法的同步不須要顯示指定同步監視器,由於它的同步監視器就是當前類的對象,也就是this。

3.鎖釋放

有鎖定就須要有釋放,同步監視器的鎖釋放的事件有如下狀況:

  1. 線程的同步塊/方法執行結束
  2. 線程的同步塊/方法執行過程當中拋出異常或者出現ERROR
  3. 線程的同步塊/方法中執行到return、break之類的終止代碼
  4. 線程的同步塊/方法中執行了同步監視器對象的wait()方法

而不釋放的事件也有以下:

  1. 線程的同步塊/方法中執行時,程序中執行了帶有sleep()\yield()等暫停操做。
  2. 線程的同步塊/方法中執行時,調用了suspend()掛起線程。

4. 同步鎖

對於基本的同步問題,synchronized就能夠知足,可是須要對線程的同步有更強大的操做,就須要到同步鎖Lock了
Lock是控制多線程對共享資源進行訪問的工具,一般,所提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源以前首先要得到Lock對象。
Lock針對不一樣的使用場景提供了多種類/接口,主要有如下:

  1. Lock
  2. ReentrantLock
  3. ReadWriteLock
  4. ReentrantReadWriteLock
1. Lock接口

Lock接口提供了幾個方法來操做鎖:

 1package java.util.concurrent.locks;
2import java.util.concurrent.TimeUnit;
3//Lock接口
4public interface Lock {
5    //獲取鎖。若是鎖已被其餘線程獲取,則進行等待
6    void lock();
7    //獲取鎖,在等待過程當中能夠相應中斷等待狀態
8    void lockInterruptibly() throws InterruptedException;
9    //嘗試獲取鎖,返回true爲得到成功,返回false爲獲取失敗
10    //它和lock()不同的是,它不會一直等待,而是嘗試獲取,當即返回
11    boolean tryLock();
12    //嘗試得到鎖,若是獲取不到就等待time時間
13    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
14    //釋放鎖
15    void unlock();
16}
複製代碼
2. ReentrantLock

可重入鎖。意思是同一個線程能夠屢次獲取同一個鎖,雖然synchronized也屬於可重入鎖,可是synchronized是在獲取鎖的過程當中是不可中斷的,而ReentrantLock則能夠。
ReentrantLock是惟一實現了Lock接口的類,所以咱們在能夠這樣建立一個Lock對象:

1Lock l=new ReentrantLock();
複製代碼

ReentrantLock的默認狀態和synchronized得到的屬於非公平鎖(搶佔式得到鎖,先等待(調用lock())的線程不必定先得到鎖,而公平鎖則是先得到lock的線程現貨的鎖)。可是ReentrantLock能夠設置爲公平鎖,如:

1//公平鎖
2Lock l1=new ReentrantLock(true);
3//非公平鎖
4Lock l2=new ReentrantLock(false);
複製代碼
3. ReadWriteLock

顧名思義,它叫作讀寫鎖,是一個接口,用來管理讀鎖和寫鎖,讀鎖也叫共享鎖,也就是說讀鎖能夠被多個線程共享,寫鎖也稱排他鎖,意思是,當一個線程得到了寫鎖,其它線程只能等待,不能共享。
前面咱們說到:多線程併發帶來同步問題,而同步問題用同步監聽器來解決問題。
但咱們發現有這樣的一個怪圈:

多線程爲了提升程序執行效率,同步監聽器爲了是多線程執行時有且只有其中一個線程能執行synchronized修飾的代碼塊或者方法,這兩個東西有着此消彼長的關係.
那麼?怎麼樣才能讓多線程能愉快的行走,而同步問題有能夠儘量少的出現呢?

其實讀寫鎖在必定程度上能解決這個難題。它的特性是:

  1. 讀讀共享
  2. 讀寫互斥
  3. 寫寫互斥

也就是說,好比程序開多個線程對一個文件進行讀寫操做時,若是用synchronized,則讀寫操做要互相等待,而有了ReadWriteLock以後
咱們能夠把讀寫的鎖操做分開,讀文件操做用讀鎖,寫文件操做用寫鎖,
這樣就能夠快運行效率了。

咱們來看它的源碼:

1public interface ReadWriteLock {
2 //獲取讀鎖
3 Lock readLock();
4 //獲取寫鎖    
5 Lock writeLock();
6}
複製代碼

只有一個獲取讀鎖和一個獲取寫鎖的接口方法,接口的存在得有有類實現它纔有意義,咱們看下一個類:

4. ReentrantReadWriteLock

ReentrantReadWriteLock是ReadWriteLock接口的實現類,當咱們要建立一個ReadWriteLock的鎖時,一般:

1ReadWriteLock rl=new ReentrantReadWriteLock();
複製代碼

前面說到ReentrantLock是Lock的實現類,ReentrantLock是一種排它鎖,也就是說某個時間內,只有容許一個線程訪問(可是這個線程能夠同時訪問屢次),而ReentrantLock是讀寫鎖,也就是說在同一時間內,容許多個線程同時獲取讀鎖進行操做(但不容許讀寫、寫寫同時操做),在某些業務場景(好比讀操做遠高於寫操做)下,ReentrantReadWriteLock會比ReentrantLock有更好的性能和併發。
ReentrantReadWriteLock主要有如下特效:

  1. 能夠設置公平鎖和非公平鎖。
1//公平鎖
2ReadWriteLock rl=new ReentrantReadWriteLock(true);
3//非公平鎖
4ReadWriteLock rl=new ReentrantReadWriteLock();
複製代碼
  1. 可重入鎖。
    2.1 同一個讀線程可屢次得到讀鎖
    2.2 同一個寫線程能夠屢次得到寫鎖或者讀鎖
  2. 可中斷性:就是說能夠在獲取鎖期間中斷操做
  3. 能夠鎖降級:也就是寫鎖可降爲讀鎖

7. 線程通訊

當線程在程序中執行時,線程的調度有一些不肯定性,也就是在常規狀況沒法準確的控制線程之間的輪換執行時機,所以Java提供了一些機制來便於開發者控制線程的協調運行。

  1. synchronized修飾的方法/代碼塊中使用wait()、notify()、notifyAll()來協調
  2. 使用condition控制
  3. 使用阻塞隊列控制
1. synchronized修飾方法/代碼塊中使用wait()、notify()、notifyAll()協調

實際上,wait、notify、notifyAll是定義在Object類的實例方法他們只能在synchronized的代碼塊/方法中使用,用來控制線程。

  1. wait: 持有鎖的線程準備釋放對象鎖權限,釋放cpu資源並進入等待。
  2. notify:持有對象鎖的線程1即將釋放鎖,通知jvm喚醒某個競爭該鎖的線程2。線程在 synchronized 代碼做用域結束後,線程2直接得到鎖,其餘競爭線程繼續等待(即便線程X同步完畢,釋放對象鎖,其餘競爭線程仍然等待,直至有新的notify ,notifyAll被調用)。
  3. notifyAll:持有鎖的線程1準備釋放鎖,通知jvm喚醒全部競爭該鎖的線程,線程1在synchronized 代碼做用域結束後,jvm經過算法將對象鎖權限指派給某個線程2,全部被喚醒的線程再也不等待。線程1在synchronized 代碼做用域結束後,以前全部被喚醒的線程都有可能得到該對象鎖權限,這個由JVM算法決定。
2. 使用condition控制

對於用Lock來作同步工做的狀況,Java提供了condition類來協助控制線程通訊。condition的實例是由Lock對象來建立的,

1//建立一個lock對象
2Lock l=new ReentrantLock();
3//建立一個condition實例
4Condition con=l.newCondition();
複製代碼

Condition類有如下方法:

  1. await():相似於wait(),致使當前線程等待,知道其它線程代用該Condition的signal()或signalAll()來喚醒該線程
  2. signal():喚醒此Lock對象上等待的單個線程,若是全部線程都在該Lock對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,只有當前線程放棄對該Lock對象的鎖定後才能夠執行被喚醒的線程
  3. signalAll():喚醒在此Lock對象上等待的全部線程,只有當前線程放棄對該Lock對象的鎖定後,才能夠執行被喚醒的線程。
3. 使用阻塞隊列控制

在Java5中提供了一個接口:BlockingQueue,它是做爲線程同步的一個工具而產生,當生產者線程試圖向BlockingQueue中放入元素時,若是該隊列已滿,則線程被阻塞,當消費者線程試圖從BlockingQueue中取出元素時,若是隊列爲空,則線程被阻塞。
BlockingQueue接口源碼:

 1public interface BlockingQueue<Eextends Queue<E{
2    boolean add(E e);
3    boolean offer(E e);
4    void put(E e) throws InterruptedException;
5    boolean offer(E e, long timeout, TimeUnit unit)
6        throws InterruptedException
;
7    take() throws InterruptedException;
8    poll(long timeout, TimeUnit unit)
9        throws InterruptedException
;
10    int remainingCapacity();
11    boolean remove(Object o);
12    public boolean contains(Object o);
13    int drainTo(Collection<? super E> c);
14    int drainTo(Collection<? super E> c, int maxElements);
15}
複製代碼

其中支持阻塞的有兩個:

  1. take():嘗試從BlockingQueue頭部獲取元素
  2. put(E e):嘗試把e放入BlockingQueue中

BlockingQueue接口的實現類有:

  1. ArrayBlockingQueue:數組阻塞隊列
  2. LinkedBlockingQueue:鏈表阻塞隊列
  3. PriorityBlockingQueue:帶有排序性的非標準阻塞隊列
  4. SynchronousQueue:同步隊列,讀寫不能同時,只能交替執行
  5. DelayQueue:特殊的阻塞隊列,它要求集合元素都實現Dely接口

阻塞隊列平時用得少,就僅僅講述一些基本原理和使用方法,例子再也不贅述。

8. 線程池

線程池的產生和數據庫的鏈接池相似,系統啓動一個線程的代價是比較高昂的,若是在程序啓動的時候就初始化必定數量的線程,放入線程池中,在須要是使用時從池子中去,用完再放回池子裏,這樣能大大的提升程序性能,再者,線程池的一些初始化配置,也能夠有效的控制系統併發的數量。
Java提供了一個Executors工廠類來建立線程池,要新建一個線程池,主要有如下幾個靜態方法:

  1. newFixedThreadPool:可重用、有固定線程數的池子
  2. newCachedThreadPool:帶有緩存的池子
  3. newSingleThreadExecutor:只有一個線程的池子
  4. newScheduledThreadPool:可指定延後執行的池子

關於每一個方法具體使用以及參數,再次就不贅述了,有興趣的筒子直接進入Executors類就能夠看到了。

9. 線程安全

什麼是線程安全?
在多線程環境下,多個線程同時訪問共享數據時,某個線程訪問的被其它線程修改了,致使它使用了錯誤的數據而產生了錯誤,這就引起了線程的不安全問題。
而當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些進程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
你們是否記得,不論是老師的課後習題仍是面試筆試題,常常都會出現「StringBuilder、StringBuffer是否線程安全」這樣的問題?
咱們來查看各自的源碼看看究竟吧。
StringBuffer的append方法:

1@Override
2    public synchronized StringBuffer append(String str) {
3        toStringCache = null;
4        super.append(str);
5        return this;
6    }
複製代碼

StringBuilder的append方法:

1 @Override
2    public StringBuilder append(String str) {
3        super.append(str);
4        return this;
5    }
複製代碼

再看看它們的super.append源碼:

1public AbstractStringBuilder append(String str) {
2        if (str == null)
3            return appendNull();
4        int len = str.length();
5        ensureCapacityInternal(count + len);
6        str.getChars(0, len, value, count);
7        count += len;
8        return this;
9    }
複製代碼

能夠看出,二者的append方法區別就在於前者有synchronized修飾,這意味着多個線程能夠同時訪問這個方法時,前者是阻塞運行的,然後者是能夠同時運行而且同時訪問count,所以就有可能致使count錯亂。因而可知:

StringBuffer 是線程安全的,可是因爲加了鎖,致使效率變低。
StringBuilder 是線程不安全的,在單線程環境下,效率很是高。

既然已經從根本知道了什麼是線程安全,那麼Java是如何解決線程安全問題的呢?
從Java5開始,增長一了些線程安全的類來處理線程安全的問題,如:

  1. ThreadLocal
  2. ConcurrentHashMap
  3. ConcurrentSkipListMap
  4. ConcurrentSkipListSet
  5. ConcurrentLinkedQueue
  6. ConcurrentLinkedDeque
  7. CopyOnWriteArrayList
  8. CopyOnWriteArrayList
  9. CopyOnWriteArraySet
  10. CopyOnWriteHashMap

10. ThreadLocal

ThreadLocal表明一個線程局部變量,經過把數據放在ThreadLocal中就可讓每一個線程建立一個該變量的副本,從未避免併發訪問的線程安全問題。
維持線程封閉性的一種方法是使用ThreadLocal。它提供了set和get等訪問方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以get方法老是返回由當前執行線程在調用set時設置的最新值。
它提供三個方法:

  1. T get():返回此線程局部變量中當前線程副本中的值。
  2. remove():刪除此線程局部變量中當前線程的值。
  3. set(T t):設置此線程局部變量中當前線程副本中的值。

舉個栗子:建立一個帶有ThreadLocal的類:

 1public class TestThreadLocal  {
2    // 副本
3    private ThreadLocal<Integer> countLoacl = new ThreadLocal<Integer>();
4    public TestThreadLocal(Integer num) {
5        countLoacl.set(num);
6    }
7    public Integer getCount() {
8        return countLoacl.get();
9    }
10    public void setCount(Integer num) {
11        countLoacl.set(num);
12    }
13}
複製代碼

這樣子建立的類帶有ThreadLocal的countLoacl,在多個線程同時消費這個對象時,ThreadLocal會爲每一個線程建立一個countLoacl副本,這樣就能夠避免多線程之間的資源競爭而致使安全問題了。

以爲本文對你有幫助?請分享給更多人
關注「編程無界」,提高裝逼技能

相關文章
相關標籤/搜索