一、線程和進程
(1)進程
當一個程序進入內存,即可變成一個進程。
進程包含三大特性:
- 獨立性
每個進程都有自己私有獨立的內存地址,沒有經過進程本身允許,一個用戶進程是不可以訪問其它進程的地址空間的 - 動態性
程序是一個靜態指令集合,而進程是一個活動的指令集合,在進程中加入時間的概念,使得進程有了生命周期。 - 并發性
在一個處理器上可以并發運行多個進程
*實際上對于一個cpu而言在同一時刻,只有一個程序在執行,cup是在多個進程間輪換著執行的,cpu輪換速度很快,看著很像并發的運行。
(2)線程
線程可以看作一個輕量級的進程,線程是進程的最小執行單元。線程也是并發獨立的執行流。
(3)線程的優勢
- 進程之間不能共享內存,線程之間共享內存非常容易
- 系統創建進程時要重新為他分匹配資源,但是創建線程代價要小的多。
二、線程的創建和啟動
(1)繼承Thread類
- 繼承Thread類并重寫run方法
- 創建Thread子類的實例
- 調用線程對象的start方法
- Thread.currentThread可以獲取當前執行的線程
- getName可以獲取當前線程的名字
setName可以設置線程的名字
注意:使用繼承Thread類的方法創建線程類時,多個線程之間無法共享線程類的實例變量
(2)實現Runnable接口創建線程
- 定義Runnable的實現類,并重寫該接口的run方法
- 創建實現類的實例,并此實例作為Thread的target來創建線程對象
注意:使用Runnable方式創建可以多個線程間共享線程類的實例對象
(3)使用Callable和Future創建線程
- 先定義Callable的實現類
- 將Callable實現類的實例傳入FutureTask的實例中
- 將FutureTask的實例作為target創建線程類
public class MyThreadTest {
public static void main(String[] args) {
FutureTask<Integer> task=new FutureTask<Integer>(new Callable<Integer>(){
public Integer call() {
int i=0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
return i;
}
});
new Thread(task).start();
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
try {
System.out.println("返回值"+task.get());
}catch(Exception e) {
e.printStackTrace();
}
}
}
(4)創建三種線程方式的對比
采用Runnable和Callable接口方式的優缺點
- 只是實現了接口還可以繼承其它類
- 多個線程可以共享一個target,非常適合多個相同的線程處理同一份資源的情況
- 劣勢是編程稍微復雜,訪問當前線程必須使用Thread.currentThread的方式
三、線程的生命周期
- 新建
- 就緒
- 運行
- 阻塞
- 死亡
(1)新建和就緒狀態
當使用new關鍵創建一個線程時,該線程就是新建狀態,此時線程對象僅僅由java虛擬機分配內存初始化成員變量,沒有表現出出任何動態特性,程序也不會執行執行體里的代碼。
當線程對象調用start()方法以后,線程進入就緒狀態,Java虛擬機會為其創建方法調用棧和程序計數器,處于此狀態的線程并沒有運行,而是可以運行了,何時運行取決于JVM中的線程調度器。
注意:只能對處于新建狀態的線程類進行start方法
(2)運行和阻塞狀態
如果處于就緒狀態的線程獲得了cpu,開始執行run方法里的執行體,此時線性為運行狀態
發生如下情況時,線程進入阻塞狀態
- 線程調用sleep方法,主動放棄所占用的管理器資源
- 線程調用了阻塞式的io方法,在該方法返回之前,該線程阻塞
- 線程企圖獲得一個同步管理器,但該同步管理器被其它線程所持有
- 線程在等待某個通知(notify)
- 程序調用了線程的suspend方法將線程掛起。(該方法容易造成死鎖,避免使用該方法)
當前線程阻塞之后,其它線程可以獲得執行的機會,被阻塞的線程會在合適的機會進入就緒狀態
針對以上的幾種情況,發生如下特定情況可以進入就緒狀態: - sleep的時間到了
- 阻塞的io已經有了返回結果
- 線程成功的獲得同步管理器資源
- 線程正在等待通知,其它線程發送了通知
-
處于掛起狀態的線程調用了resume恢復方法
(3)線程死亡
線程有三種方式結束:
- 當run方法或者call方法執行完成
- 線程拋出Exception或者Error
- 或者調用Stop方法(容易造成死鎖不推薦使用)
注意:子線程不受主線程的影響,子線程和主線程擁有相同的地位
可以調用isAlive方法檢測該線程是否死亡
四、控制線程
(1)join線程
join方法是讓一個線程等待另一個線程完成的方法。當在某個線程執行流當中調用其它線程的join方法,調用的線程會阻塞起來,直到被join方法join進來的線程執行完畢。
join方法通常由使用線程的程序調用,可以將大問題劃分成小問題,每個小問題都分配一個線程。當所有小問題執行完畢以后,再調用主線程進行下一步。
public class JoinTest {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
public void run() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
};
for(int j=0;j<100;j++) {
System.out.println(Thread.currentThread().getName()+j);
if(j==20) {
Thread tr=new Thread(runnable);
tr.start();
try {
tr.join();
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
}
}
}
join有三個重載的方法:
join();等待被join的線程執行完成
join(long millis);如果再指定秒數內被join的線程還沒執行完成則不再等待
join(long millis,int nanos);等待被join的線程millis毫秒加nanos微毫秒(很少使用)
(2)后臺線程
有一種線程,它在后臺運行,它的任務是為其它線程提供服務,被稱為后臺線程。
特征:如果所有前臺線程都死亡,后臺線程自動死亡
調用Thread對象的setDaemon(true)方法可以將一個線程設置成后臺進程。
注意:setDaemon方法必須在start方法之前調用
public class ThreadDemo1 extends Thread{
public void run() {
for(int i=0;i<200;i++) {
System.out.println(this.getName()+i);
}
}
public static void main(String[] args) {
ThreadDemo1 demo=new ThreadDemo1();
demo.setDaemon(true);
demo.start();
for(int j=0;j<20;j++) {
System.out.println(Thread.currentThread().getName()+j);
}
}
}
Thread還提供了isDaemon()方法判斷是否為后臺進程
(3)線程睡眠:sleep
如果需要讓線程暫停一段時間并進入阻塞狀態,則可以通過調用Thread類的靜態方法sleep實現
public class SleepTest {
public static void main(String[] args) {
for(int i=0;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+i);
if(i==20) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
}
}
}
此外還有一個方法與sleep類似,yield方法,它可以讓正在執行的線程暫停,但是不會進入阻塞狀態,而是直接進入就緒狀態。yield只會讓線程管理器重新調度一次,很有可能線程調用yield方法暫停之后,馬上又被線程調度器調度出來運行。當調用了yield方法之后只有優先級相同或者更高才有可能獲得運行的機會
兩者的區別:
- sleep暫停之后會給其他線程機會,不用在乎優先級,而yield只會給優先級相同或更高的機會
- sleep會讓線程進入阻塞狀態,而yield會讓線程轉到就緒狀態
- sleep方法拋出了InterruptedException,而yield沒有拋出異常
- sleep比yield有更好的移植性,通常不建議使用yield控制并發線程
(4)改變線程優先級
每個線程都有優先級,優先級高的線程得到的執行的機會就多。
每個線程都默認與創建它的父線程一樣,在默認的情況下,main線程具有普通優先級。
Thread類提供了setPriority(int newPriority)設置和返回線程的優先級,其中傳進去的參數可以是1到10之間的整數,也可以是Thread下的三個靜態常量。
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
五、線程同步
當并發線程共同訪問一個數據時,有可能出現問題,出現并發安全問題
(1)同步代碼塊
Java多線程引入了同步監視器,使用同步監視器的通用方法就是同步代碼塊。語法如下:
synchronized(obj){
//此處的代碼就是同步代碼塊
}
synchronized括號里面的就是同步監視器,線程開始執行前必須獲得同步監視器的鎖定。Java雖然可以使用任何對象作為同步監視器,但是推薦使用有可能并發訪問的共享資源作為同步監視器
(2)同步方法
與同步代碼塊對應,Java多線程還提供了同步方法,使用方法就是用synchronized關鍵字修飾方法,同步方法的同步監視器就是this
線程安全的特征有如下幾個特征:
- 該類的對象可以被多個線程安全的訪問
- 每個線程調用該對象任何方法都得到正確的結果
- 每個線程調用該對象任何方法之后,該對象依然保持合理的狀態
注意:synchronized可以修飾代碼塊可以修飾方法,但是不可以修飾構造器和成員變量
可變類的線程安全是犧牲程序的運行效率的
解決方案:
不要對可變類的所有方法都進行同步,只對改變競爭資源的方法同步
可以寫兩個,一個安全的,一個不安全的,單線程情況下用不安全的,多線程時使用安全的
六、釋放同步監視器的鎖定
程序無法顯式的釋放同步監視器,如下情況會釋放:
- 同步的方法或代碼塊執行完畢或者遇到return,break關鍵字
- 同步的方法代碼塊內拋出異常或者Error
- 在執行同步代碼塊或方法時,執行了同步監視器的wait方法,當前線程暫停,并釋放同步監視器
七、同步鎖
Lock是控制多個線程對共享資源的進行訪問的工具。Java提供了ReentrantLock、ReetrantReadWriteLock、StampedLock。一般常用的就是ReetrantLock。
使用方法:
private final ReetrantLock lock=new ReetrantLock();
lock.lock();
try{
//執行的同步代碼
}finally{
lock.unLock();
}
八、死鎖
當兩個線程互相等待對方釋放同步監視器時就會發生死鎖,典型的哲學家就餐問題。
九、線程通信
(1)傳統的線程通信
通過使用Object的wait(),notify(),notifyAll()方法來控制,這三個方法只能由同步監視器來調用。當使用同步方法時,同步監視器就是this,可以直接調用。當同步代碼塊時,只能由Synchronized括號里面的同步監視器調用
(2)使用Condition控制線程通信
當使用Lock鎖時,沒有同步監視器,也就不能使用wait,notify,notifyAll方法,所以使用Condition對象來進行通信,可以通過Lock對象的newCondition方法獲得
Condition提供了三個方法:
await:類似于隱式同步監視器上的wait方法,導致當前線程等待,直到其它線程調用該Condition的signal或者signalAll方法喚醒
signal:喚醒次Lock對象上等待的單個線程。如果所有線程都在改Lock上等待,則會選擇喚醒其中一個線程。
signalAll:喚醒在此Lock對象上等待的所有線程。
(3)使用阻塞隊列控制線程通信
BlockingQueue接口,它不是作為容器使用的,而是作為線程同步的工具。它具有一個特征:當生產者線程想往里放入元素時,如果隊列已滿,則該線程被阻塞,如果消費者想要取出元素時,如果隊列已空則線程阻塞。
分別設有put(E e)方法和take()方法
十、線程池
java5以后提供了一個Executor工廠來生產線程池,該工廠類包含幾個靜態的方法來創建線程池:
- newCachedThreadpool():創建一個具有緩存功能的線程池,系統根據需求創建線程,這些線程將緩存在線程池中
- newFixedThreadPool(int nThread):創建一個可重用,固定數量線程的線程想池
- newSingleThreadExecutor():創建一個只有單線程的線程池
- newScheduledThreadPool(int nThread):創建一個固定數量線程的線程池,它可以在指定延遲時間后執行
- newSingleThreadScheduledExecutor():創建一個單線程的線程池,它可以在指定延遲以后執行
- ExecutorServicenewWorkStealing(int parallelism)創建持有足夠的線程的線程池來支持給定的并行級別,該方法還會使用多個隊列來減少競爭
- ExecutorServicenewWorkStealingpool()該方法是前一個方法的簡化的版本,如果有四個cpu則目標并行級別設為4
十一、ThreadLocal類
ThreadLocal是線程局部變量的意思,它可以為每一個線程提供一個變量值的副本使每一個線程都可以獨立改變副本,不會和其他線程沖突。
ThreadLocal提供了三個方法:
T get():返回此線程局部變量副本的值
void remove():刪除此線程局部變量中當前的值
void set(T value):設置此線程局部變量中當前的副本值