線程
線程是操作系統(tǒng)的最小調度單元纯衍。操作系統(tǒng)在運行一個程序的時候會為其創(chuàng)建一個進程,如創(chuàng)建一個java程序操作系統(tǒng)就會創(chuàng)建一個進程所计,線程也叫做輕量級進程运吓,在一個進程里面可以創(chuàng)建多個線程。這些線程擁有各自的計數器兄春,堆棧和局部變量等屬性澎剥,并且能夠訪問共享的內存變量,處理器在這些線程之間高速切換使得使用者感覺這些線程在同時進行
線程優(yōu)先級
現代操作及系統(tǒng)基本采用時分的形式調度運行的線程赶舆,操作系統(tǒng)會分出一個個時間片哑姚,線程會分配到若干個時間片,時間片用完了就會發(fā)生縣城調度芜茵,并等待下次分配叙量。
線程分配到的時間騙多少也就決定了線城市用處理器資源的多少,縣城優(yōu)先級就是決定線程需要分配多少處理器資源的線程屬性九串。
這個屬性由一個整型成員變量priority來控制绞佩。
線程優(yōu)先級不能作為程序正確性的依賴,因為有些操作系統(tǒng)會完全不用理會優(yōu)先級的定義猪钮。
線程的狀態(tài)
線程一共有6種狀態(tài)品山,同一時間只能處于一種狀態(tài)。
- NEW 初始狀態(tài)烤低,線程被構建但是還沒有調用start()方法
- RUNNABLE 運行狀態(tài)
- BLOCKED 阻塞狀態(tài)
- WAITING 等待狀態(tài) 等待其它線程
- TIME_WAITING 超時等待肘交,可以再指定的時間自行返回。
- TEMINATED 終止狀態(tài)
當線程調用同步方法的時候扑馁,沒有獲取鎖時涯呻,將會進入阻塞狀態(tài)。執(zhí)行runnable的run()之后會進入終止狀態(tài)檐蚜。
線程的啟動和終止
一個新構造的線程對象是由其parent線程來進行空間分配的魄懂。會分配一個唯一得ID來標識這個child線程。線程對象被初始化好了之后闯第,在堆內存中等待運行市栗。
啟動線程
線程在初始化完成之后,調用start()方法就可以啟動,這個方法的含義是:當前線程(parent)同步告知java虛擬機填帽,只要線程規(guī)劃器空閑蛛淋,應該立即啟用調用start()方法的線程
線程中斷
中斷好比其它線程對這個線程打了個招呼,其它線程通過調用該線程的interrupt()方法對其進中斷操作篡腌。
過期的suspend(),resume(),stop()
package com.page94;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Deprecated {
public static void main(String[] args) throws Exception{
DateFormat format = new SimpleDateFormat("HH:mm:ss");
Thread printThread = new Thread(new Runner(),"PrintThread");
printThread.setDaemon(true);
printThread.start();
TimeUnit.SECONDS.sleep(3);
printThread.suspend();
System.out.println("main suspend PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.resume();
System.out.println("main resume PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.stop();
System.out.println("main stop PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
}
static class Runner implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
DateFormat format = new SimpleDateFormat("HH:mm:ss");
while(true){
System.out.println(Thread.currentThread().getName()+" Run at"+format.format(new Date()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
運行結果:
PrintThread Run at17:20:41
PrintThread Run at17:20:42
PrintThread Run at17:20:43
main suspend PrintThread at17:20:44
PrintThread Run at17:20:44
main resume PrintThread at17:20:47
PrintThread Run at17:20:48
PrintThread Run at17:20:49
main stop PrintThread at17:20:50
但是不建議使用 原因如下:
- 如在調用suspend()后線程不會釋放已經占有的資源(比如鎖)褐荷,而是占有資源的情況下sleep,容易引發(fā)死鎖嘹悼。
- stop()方法終結線程時不會寶恒線程的資源正常釋放叛甫,只是說有這個機會。
暫脱罨铮恢復操作 可以由 等待/通知 機制來代替其监。
安全的終止線程
通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷的將線程停止限匣,這樣做會更優(yōu)雅和安全抖苦。
線程間通信
線程開始執(zhí)行便擁有了自己的棧空間米死。
volatile 和 synchronized關鍵字
java支持多個線程訪問一個對象或者對象的成員變量 每個線程都擁有這些變量的拷貝锌历,會導致一個線程看到的變量并不一定是最新的。
volatile可以用來修飾字段(成員變量) 峦筒,告知程序任何對該變量的訪問都要從共享內存中獲取究西,而且對他的改變必須同步刷新回共享內存,他能保證所有線程對該變量的可見性勘天。一般涉及到多個線程要對一個變量訪問就將其定為volatile怔揩。
synchronized可以修飾方法或以同步塊的形式來進行使用,它確保多個線程在同一時刻脯丝,只能有一個線程處于方法或同步塊中,保證了線程對變量訪問的可見性和排他性伏伐。
加鎖的本質是對一個對象的監(jiān)視器(monitor)進行獲取宠进,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器藐翎。
任何一個對象都擁有自己的監(jiān)視器材蹬,當這個對象被同步塊或同步方法調用時,執(zhí)行方法的線程必須先獲得該對象的監(jiān)視器才能進入同步塊或者同步方法吝镣。獲取不到的線程將會被阻塞在同步快和同步方法的入口處(同步隊列)堤器,進入BLOCKED狀態(tài)。直到其他獲得了鎖的線程(前驅)釋放了鎖末贾,這個釋放操作會喚醒阻塞在同步隊列中的線程闸溃,讓其重新嘗試獲取鎖。
等待/通知機制
java中典型的消息傳遞機制。是任何java對象所具備的辉川。
一個線程A 調用了對象O的 wait()方法進入等待狀態(tài)表蝙,而另一個線程B調用了對象O的 notify( )方法,線程A接到通知后從對象O的wait()方法返回乓旗,進而執(zhí)行后續(xù)操作府蛇。兩個線程通過對象O來完成交互,對象上的wait和notify的關系如同開關信號屿愚,用來完成等待方和通知方之間的交互工作汇跨。
等待/通知范式
等待方
- 獲取對象鎖
- 條件不滿足就wait()
- 條件滿足則執(zhí)行對應邏輯
通知方
- 獲得對象鎖
- 改變條件
- 通知所有等待在對象上的線程
管道輸入/輸出流
區(qū)別于文件輸入/輸出流或者網絡輸入/輸出流不同之處在于,主要用于線程之間的數據傳輸妆距,傳輸媒介為內存穷遂。
其主要有四種實現 PipedOutputStream、PipedInputStream毅厚、PipedReader塞颁、PipedWriter,前兩種面向字節(jié)后兩種面向字符吸耿。
Therad.join()
線程A執(zhí)行了thread.join()的含義是:當前線程A等待thread線程終止后才從thread.join()返回祠锣。這個方法也有超時方法的重載。
ThreadLocal
線程變量咽安,是一個以ThreadLocal對象為鍵伴网,任意對象為值得儲存結構。一個線程可以根據ThreadLocal對象查詢綁定在這個線程上的一個值妆棒。
線程應用實例
等待超時模式
在等待通知范式上 用 wait(long t)來進行超時等待澡腾。
數據庫連接池
數據庫連接池負責分配,管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是重新建立一個。
線程池
線程池的本質是使用了一個線程安全的工作隊列連接工作者線程和客戶端線程糕珊,客戶端線程將任務放入工作隊列后便返回动分,工作者縣城不斷地從工作隊列上取工作來執(zhí)行,當工作隊列為空的時候红选,所有的工作者線程均等待在工作隊列上澜公。當有客戶端提交了一個任務之后會通知任意一個工作者線程。
線程實現的三種方式
- 使用內核線程實現
- 使用用戶線程實現
- 使用用戶線程加輕量級進程混合實現
java線程的調度
java使用的線程調度方式是搶占式的調度喇肋,是系統(tǒng)自動完成的坟乾,但是我們還是可以建議系統(tǒng)給某些線程多分配或少分配時間---設置現成的優(yōu)先級。但是線程優(yōu)先級并不靠譜蝶防,因為java線程是通過映射到系統(tǒng)的原生線程上來實現的甚侣,所以線程調度的最終結果取決于操作系統(tǒng),如上面線程優(yōu)先級所提到的 间学,線程優(yōu)先級不能作為程序正確性的依賴殷费,因為有些操作系統(tǒng)會完全不用理會優(yōu)先級的定義印荔。
線程安全
什么是線程安全?
當多個線程訪問一個對象時宗兼,如果不用考慮這些線程在運行時環(huán)境下的調度和替換執(zhí)行躏鱼,也不需要進行額外的同步,或者在調用方法的時候進行任何其他協(xié)調操作殷绍,調用這個對象的行為都可以獲得正確的結果染苛,那么對象是線程安全的。
按照線程安全的安全程度可以把java中的對共享數據的操作分為以下五類
- 不可變
不可變的對象一定是線程安全的主到,比如final關鍵字在之前的總結中提到過只要一個不可變的獨享被正確的構建出來(沒有出現this逃逸的情況)其外部可見狀態(tài)永遠不可變茶行。 - 絕對線程安全
實際上標明自己是線程安全的類,也不是絕對安全登钥。比如Vector畔师,因為他的add get size方法都被synchronized修飾的,但是即使所有方法都被修飾成同步的牧牢,也不意味著調用這些方法永遠不需要同步手段了看锉,因為多個線程調用這些方法如果不做同步手段。 - 相對線程安全
保證對一個對象單獨地操作是線程安全的塔鳍,在調用的時候不需要做額外的保障措施伯铣,但是對于一些特定順序的連續(xù)調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性轮纫,在java中大部分線程安全類都屬于這種類型腔寡,如 hashtable vector等 - 線程兼容
指的是對象本身并不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在并發(fā)環(huán)境中可以安全地使用掌唾,如ArrayList放前、HashMap等 - 線程對立
指的是無論調用端是否采用同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼糯彬。
線程安全的實現方法
同步指的是多個線程并發(fā)的訪問共享數據時凭语,保證共享數據在同一時刻只能被一個線程訪問。
互斥是實現同步的一種手段撩扒。
互斥是因叽粹,同步是果,互斥是方法却舀,同步是目的。
互斥同步
1. Synchronized的實現原理
Synchronized關鍵詞在編譯后锤灿,會在同步塊的前后分別形成 monitorenter和monitorexit這兩個字節(jié)碼指令挽拔,這兩個字節(jié)碼需要一個reference類型變量來指定需要鎖定的對象,如果synchronized明確指明了鎖定的對象但校,那這個reference就是這個對象的引用螃诅。如果沒有明確指定,就看它修飾的是方法還是實例方法,去取對應的類方法或對象實例來作為鎖定的對象术裸。
執(zhí)行monitorenter時倘是,首先嘗試獲取對象鎖,如果當前線程已經擁有了這個對象的鎖袭艺,則將計數器+1,相應執(zhí)行monitorexit的時候就將計數器-1搀崭。當計數器為0時,鎖就被釋放猾编。如果獲取對象鎖失敗瘤睹,那當前線程就要阻塞等待,知道對象鎖被另一個線程釋放為止答倡。
前面也提到過因為java線程是通過映射到系統(tǒng)的原生線程上來實現的轰传,,所以如果要阻塞或喚醒一個線程瘪撇,都需要操作系統(tǒng)來幫忙获茬,就需要從用戶態(tài)轉換到和心態(tài)中,轉換狀態(tài)會需要消耗很多處理器的時間倔既。所以synchronized在java中是一個重量級的操作恕曲。不過也有相應的優(yōu)化,就是在通知操作系統(tǒng)阻塞線程之前加入一段自選等待過程叉存,避免頻繁的切入和心態(tài)码俩。
2.J.U.C中的ReentrantLock
重入鎖,是一個表現在API層面的互斥鎖歼捏,通過lock(),unlock()try/finally稿存,來完成。而synchronized是表現在原生語法層面的互斥鎖瞳秽。reentrantLock中有一些更高級的功能如下:
- 等待可中斷
就是線程等待持有鎖的線程釋放鎖等了很久就可以選擇放棄等待瓣履,去處理其他事情。 - 公平鎖
多個線程等待同一個鎖的時候练俐,必須按照申請鎖的時間順序來一次獲得鎖袖迎,synchronized中的鎖默認情況就是非公平的,有帶布爾類型的構造函數可以使synchronized使用公平鎖腺晾。 - 鎖綁定多個條件
指的是一個ReentrentLock對象可以同時綁定多個Condition對象燕锥,而Synchronized中需要關聯(lián)多個條件,則需要額外的添加鎖悯蝉,而ReentrantLock只需要多次調用newCondition()即可归形。
非阻塞同步
互斥同步的不足之處
互斥同步的主要原因就是進行線程的阻塞喚醒的消耗所帶來的性能問題,所以這種同步也稱為阻塞同步鼻由∠玖瘢互斥同步比較悲觀厚棵,他認為只要不去進行正確的同步措施,就會出現問題蔼紧,無論共享數據是否會出現競爭婆硬,都要進行加鎖、用戶態(tài)核心態(tài)轉換奸例、維護鎖的計數器彬犯、檢查被阻塞線程是否需要喚醒等操作。
基于沖突檢測的樂觀并發(fā)策略(非阻塞同步)
先進行操作哩至,如果沒有其他線程爭用數據那么操作就成功躏嚎;如果共享數據有其它線程爭用,產生了沖突(沖突檢測)菩貌,那就再采取補償措施(如不斷重試卢佣,直到成功),這種樂觀的并發(fā)策略并不需要把線程掛起箭阶,因此這種同步操作稱為非阻塞同步虚茶。
以上說的操作 和 沖突檢測這兩個步驟具備原子性。而如果靠互斥同步來保證這個原子性就失去意義仇参,所以需要靠硬件指令來完成這件事嘹叫。如 測試并設置,獲取并增加诈乒,交換罩扇,比較并交換(CAS)等。
CAS操作
其中CAS需要三個操作數怕磨,內存位置V喂饥、舊的預期值A、新值B肠鲫。當且僅當V符合舊預期值A的時候處理器采用新值B去更新V上的值员帮,否則不執(zhí)行更新,但無論是否更新V的值都會返回舊值导饲。
CAS操作由Unsafe類里面的compareAndSwapInt()捞高、compareAndSwapLong()等幾個方法包裝提供。這個類不是提供給用戶程序調用的類渣锦,只限制了啟動類加載器加載的Class才能訪問硝岗、因此如果不采用反射的手段就只能通過其他javaAPI來訪問,如juc包里面的整數原子類袋毙,其中的CompareAndSet()和getAndIncreament()等方法辈讶。
以incrementAndGet()為例
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
其中包含一個無限循環(huán),不斷嘗試將當前值加1操作娄猫。如果失敗說明在執(zhí)行compareAndSet()的時候這個值已經被修改就是V上的值與舊值不符合贱除,就不進行替換操作,于是進入下一次循環(huán)重試媳溺。
CAS的不足之處
- ABA問題
它無法涵蓋互斥同步的所有使用場景月幌,它存在一個漏洞:如果V初次讀取的時候是A值,被其他線程賦值后它仍然是A值悬蔽,如它曾經被改成了B后來又被改成了A扯躺,CAS就會認為他沒有變過。不過可以通過加入版本號來保證CAS的正確性蝎困、不過大多數時候ABA問題不會影響程序并發(fā)的正確性录语,并且如果有ABA問題,改用傳統(tǒng)的互斥同步可能會比原子類更高效禾乘。 - 循環(huán)時間長導致系統(tǒng)開銷大
- 只能保證一個共享變量的原子操作澎埠,多個共享變量的時候可以用鎖,或者把多個共享變量合并成一個共享變量來操作 比如 i=2 j=a 合并之后 ij=2a然后進行CAS操作