本文旨在用最通俗的語言講述最枯燥的基本知識。java
全文提綱:
1.線程是什麼?(上)
2.線程和進程的區別和聯繫(上)
3.建立多線程的方法(上)
4.線程的生命週期(上)
5.線程的控制(上)
6.線程同步(下)
7.線程池(下)
8.線程通訊(下)
9.線程安全(下)
10.ThreadLocal的基本用法(下)面試
上集已經講述了Java線程的一些基本概念,本文接下來說述的是Java的一些高級應用。算法
一開始接觸「線程同步」這個概念能夠有點難以理解,咱們來舉個栗子:數據庫
爸爸開了一張銀行卡存進去10000塊錢,是留給在山東讀大學的哥哥和在河南老家讀高中的妹妹用的。哥哥前天取了2000,變成8000,妹妹昨天取了1000,剩餘7000,今天他們同時到銀行同時取錢,哥哥打開時ATM發現有7000餘額,妹妹打開時也發現是7000餘額,他們同時按下肯定取1000錢,當他們取完錢以後在查看餘額發現只有5000塊錢,都在想我只取了1000啊怎麼扣了我2000呢?
這就是生活中的「同步」問題了。
咱們把思惟轉入到這個ATM的後臺程序,幸虧後臺程序對取錢的操做作了同步動做的監聽器,能在多線程同時操做的過程當中把取錢的動做給鎖定起來,若是程序沒有處理同步問題,那兩邊的ATM的算術都是:7000-1000,結果是剩餘6000.這樣子,銀行對帳就會出錯了。編程
所以可見,併發編程不合理使用也會帶來一些弊端,而針對多線程併發的問題,Java引入了同步監視器來解決問題:當線程要執行同步代碼塊/方法以前,必須先得到對同步監視器的鎖定。
Java中鎖用在的地方有:數組
- 代碼塊
- 方法(構造器、成員變量除外)
語法:緩存
1synchronized (obj) {
2//同步內容(好比取錢的操做)
3}
複製代碼
其中obj就是同步監視器,也就是說任何線程要進入執行該代碼塊以前,首先得到對obj的鎖定,得到以後,其它線程就沒法獲取它,修改它,直到當前線程釋放位置。
好比:爸爸的銀行卡帳戶安全
1public BankCardAccount bankAccount;
2synchronized (bankAccount) {
3//對bankAccount的扣錢動做
4}
複製代碼
當哥哥和妹妹同時取錢時,就如同兩個線程在執行,當其中一個線程獲取到對bankAccount的鎖定時,另外一個線程必須等待當前線程用完以後釋放bankAccount的鎖定,才能夠得到而且修改之多線程
語法:併發
1修飾符 synchronized 返回值 方法名(形參列表){
2}
複製代碼
方法的同步不須要顯示指定同步監視器,由於它的同步監視器就是當前類的對象,也就是this。
有鎖定就須要有釋放,同步監視器的鎖釋放的事件有如下狀況:
- 線程的同步塊/方法執行結束
- 線程的同步塊/方法執行過程當中拋出異常或者出現ERROR
- 線程的同步塊/方法中執行到return、break之類的終止代碼
- 線程的同步塊/方法中執行了同步監視器對象的wait()方法
而不釋放的事件也有以下:
- 線程的同步塊/方法中執行時,程序中執行了帶有sleep()\yield()等暫停操做。
- 線程的同步塊/方法中執行時,調用了suspend()掛起線程。
對於基本的同步問題,synchronized就能夠知足,可是須要對線程的同步有更強大的操做,就須要到同步鎖Lock了
Lock是控制多線程對共享資源進行訪問的工具,一般,所提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源以前首先要得到Lock對象。
Lock針對不一樣的使用場景提供了多種類/接口,主要有如下:
- Lock
- ReentrantLock
- ReadWriteLock
- ReentrantReadWriteLock
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}
複製代碼
可重入鎖。意思是同一個線程能夠屢次獲取同一個鎖,雖然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);
複製代碼
顧名思義,它叫作讀寫鎖,是一個接口,用來管理讀鎖和寫鎖,讀鎖也叫共享鎖,也就是說讀鎖能夠被多個線程共享,寫鎖也稱排他鎖,意思是,當一個線程得到了寫鎖,其它線程只能等待,不能共享。
前面咱們說到:多線程併發帶來同步問題,而同步問題用同步監聽器來解決問題。
但咱們發現有這樣的一個怪圈:
多線程爲了提升程序執行效率,同步監聽器爲了是多線程執行時有且只有其中一個線程能執行synchronized修飾的代碼塊或者方法,這兩個東西有着此消彼長的關係.
那麼?怎麼樣才能讓多線程能愉快的行走,而同步問題有能夠儘量少的出現呢?
其實讀寫鎖在必定程度上能解決這個難題。它的特性是:
- 讀讀共享
- 讀寫互斥
- 寫寫互斥
也就是說,好比程序開多個線程對一個文件進行讀寫操做時,若是用synchronized,則讀寫操做要互相等待,而有了ReadWriteLock以後
咱們能夠把讀寫的鎖操做分開,讀文件操做用讀鎖,寫文件操做用寫鎖,
這樣就能夠快運行效率了。
咱們來看它的源碼:
1public interface ReadWriteLock {
2 //獲取讀鎖
3 Lock readLock();
4 //獲取寫鎖
5 Lock writeLock();
6}
複製代碼
只有一個獲取讀鎖和一個獲取寫鎖的接口方法,接口的存在得有有類實現它纔有意義,咱們看下一個類:
ReentrantReadWriteLock是ReadWriteLock接口的實現類,當咱們要建立一個ReadWriteLock的鎖時,一般:
1ReadWriteLock rl=new ReentrantReadWriteLock();
複製代碼
前面說到ReentrantLock是Lock的實現類,ReentrantLock是一種排它鎖,也就是說某個時間內,只有容許一個線程訪問(可是這個線程能夠同時訪問屢次),而ReentrantLock是讀寫鎖,也就是說在同一時間內,容許多個線程同時獲取讀鎖進行操做(但不容許讀寫、寫寫同時操做),在某些業務場景(好比讀操做遠高於寫操做)下,ReentrantReadWriteLock會比ReentrantLock有更好的性能和併發。
ReentrantReadWriteLock主要有如下特效:
- 能夠設置公平鎖和非公平鎖。
1//公平鎖
2ReadWriteLock rl=new ReentrantReadWriteLock(true);
3//非公平鎖
4ReadWriteLock rl=new ReentrantReadWriteLock();
複製代碼
- 可重入鎖。
2.1 同一個讀線程可屢次得到讀鎖
2.2 同一個寫線程能夠屢次得到寫鎖或者讀鎖- 可中斷性:就是說能夠在獲取鎖期間中斷操做
- 能夠鎖降級:也就是寫鎖可降爲讀鎖
當線程在程序中執行時,線程的調度有一些不肯定性,也就是在常規狀況沒法準確的控制線程之間的輪換執行時機,所以Java提供了一些機制來便於開發者控制線程的協調運行。
- synchronized修飾的方法/代碼塊中使用wait()、notify()、notifyAll()來協調
- 使用condition控制
- 使用阻塞隊列控制
實際上,wait、notify、notifyAll是定義在Object類的實例方法他們只能在synchronized的代碼塊/方法中使用,用來控制線程。
- wait: 持有鎖的線程準備釋放對象鎖權限,釋放cpu資源並進入等待。
- notify:持有對象鎖的線程1即將釋放鎖,通知jvm喚醒某個競爭該鎖的線程2。線程在 synchronized 代碼做用域結束後,線程2直接得到鎖,其餘競爭線程繼續等待(即便線程X同步完畢,釋放對象鎖,其餘競爭線程仍然等待,直至有新的notify ,notifyAll被調用)。
- notifyAll:持有鎖的線程1準備釋放鎖,通知jvm喚醒全部競爭該鎖的線程,線程1在synchronized 代碼做用域結束後,jvm經過算法將對象鎖權限指派給某個線程2,全部被喚醒的線程再也不等待。線程1在synchronized 代碼做用域結束後,以前全部被喚醒的線程都有可能得到該對象鎖權限,這個由JVM算法決定。
對於用Lock來作同步工做的狀況,Java提供了condition類來協助控制線程通訊。condition的實例是由Lock對象來建立的,
1//建立一個lock對象
2Lock l=new ReentrantLock();
3//建立一個condition實例
4Condition con=l.newCondition();
複製代碼
Condition類有如下方法:
- await():相似於wait(),致使當前線程等待,知道其它線程代用該Condition的signal()或signalAll()來喚醒該線程
- signal():喚醒此Lock對象上等待的單個線程,若是全部線程都在該Lock對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,只有當前線程放棄對該Lock對象的鎖定後才能夠執行被喚醒的線程
- signalAll():喚醒在此Lock對象上等待的全部線程,只有當前線程放棄對該Lock對象的鎖定後,才能夠執行被喚醒的線程。
在Java5中提供了一個接口:BlockingQueue,它是做爲線程同步的一個工具而產生,當生產者線程試圖向BlockingQueue中放入元素時,若是該隊列已滿,則線程被阻塞,當消費者線程試圖從BlockingQueue中取出元素時,若是隊列爲空,則線程被阻塞。
BlockingQueue接口源碼:
1public interface BlockingQueue<E> extends 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 E take() throws InterruptedException;
8 E 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}
複製代碼
其中支持阻塞的有兩個:
- take():嘗試從BlockingQueue頭部獲取元素
- put(E e):嘗試把e放入BlockingQueue中
BlockingQueue接口的實現類有:
- ArrayBlockingQueue:數組阻塞隊列
- LinkedBlockingQueue:鏈表阻塞隊列
- PriorityBlockingQueue:帶有排序性的非標準阻塞隊列
- SynchronousQueue:同步隊列,讀寫不能同時,只能交替執行
- DelayQueue:特殊的阻塞隊列,它要求集合元素都實現Dely接口
阻塞隊列平時用得少,就僅僅講述一些基本原理和使用方法,例子再也不贅述。
線程池的產生和數據庫的鏈接池相似,系統啓動一個線程的代價是比較高昂的,若是在程序啓動的時候就初始化必定數量的線程,放入線程池中,在須要是使用時從池子中去,用完再放回池子裏,這樣能大大的提升程序性能,再者,線程池的一些初始化配置,也能夠有效的控制系統併發的數量。
Java提供了一個Executors工廠類來建立線程池,要新建一個線程池,主要有如下幾個靜態方法:
- newFixedThreadPool:可重用、有固定線程數的池子
- newCachedThreadPool:帶有緩存的池子
- newSingleThreadExecutor:只有一個線程的池子
- newScheduledThreadPool:可指定延後執行的池子
關於每一個方法具體使用以及參數,再次就不贅述了,有興趣的筒子直接進入Executors類就能夠看到了。
什麼是線程安全?
在多線程環境下,多個線程同時訪問共享數據時,某個線程訪問的被其它線程修改了,致使它使用了錯誤的數據而產生了錯誤,這就引起了線程的不安全問題。
而當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些進程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
你們是否記得,不論是老師的課後習題仍是面試筆試題,常常都會出現「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開始,增長一了些線程安全的類來處理線程安全的問題,如:
- ThreadLocal
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- ConcurrentLinkedQueue
- ConcurrentLinkedDeque
- CopyOnWriteArrayList
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- CopyOnWriteHashMap
ThreadLocal表明一個線程局部變量,經過把數據放在ThreadLocal中就可讓每一個線程建立一個該變量的副本,從未避免併發訪問的線程安全問題。
維持線程封閉性的一種方法是使用ThreadLocal。它提供了set和get等訪問方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以get方法老是返回由當前執行線程在調用set時設置的最新值。
它提供三個方法:
- T get():返回此線程局部變量中當前線程副本中的值。
- remove():刪除此線程局部變量中當前線程的值。
- 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副本,這樣就能夠避免多線程之間的資源競爭而致使安全問題了。