概述
在過去單CPU時代她倘,單任務在一個時間點只能執(zhí)行單一程序擂找。之后發(fā)展到多任務階段迄损,計算機能在同一時間點并行執(zhí)行多任務或多進程奈应。雖然并不是真正意義上的“同一時間點”澜掩,而是多個任務或進程共享一個CPU,并交由操作系統(tǒng)來完成多任務間對CPU的運行切換杖挣,以使得每個任務都有機會獲得一定的時間片運行肩榕。
Java是最先支持多線程的開發(fā)的語言之一,Java從一開始就支持了多線程能力程梦,因此Java開發(fā)者能常遇到上面描述的問題場景
一点把、相關概念
-
程序與進程
程序是一組有序指令的集合橘荠,是一種靜態(tài)的概念。進程是程序的一次執(zhí)行郎逃,屬于一種動態(tài)的概念哥童。在多道程序環(huán)境下,程序的執(zhí)行屬于并發(fā)執(zhí)行褒翰,此時它們將失去封閉性贮懈,并具有間斷性,運行結果也將不可再現优训,為了能使多個程序可以并發(fā)執(zhí)行朵你,提高資源利用率和系統(tǒng)吞吐量,并且可以對并發(fā)執(zhí)行的程序加以描述和控制揣非,引入進程的概念抡医。 -
進程和線程
線程的引入主要是為了減少程序在并發(fā)執(zhí)行時所付出的時空開銷。我們知道早敬,為了能使程序能夠并發(fā)執(zhí)行忌傻,系統(tǒng)必須進行創(chuàng)建進程、撤銷進程以及進程切換等操作搞监,而進程作為一個資源的擁有者水孩,在進行這些操作時必須為之付出較大的時空開銷。
線程和進程的區(qū)別主要如下:(1) 進程是系統(tǒng)中擁有資源的一個基本單位琐驴,線程本身并不擁有系統(tǒng)資源俘种,同一進程內的線程共享進程擁有的資源。(2) 進程僅是資源分配的基本單位绝淡,線程是調度和分派的基本單位宙刘。(3) 進程之間相對比較獨立,彼此不會互相影響够委,而線程共享同一個進程下面的資源荐类,可以互相通信影響。(4) 線程的并發(fā)性更高茁帽,可以啟動多個線程執(zhí)行同程序的不同部分玉罐。 -
并行和并發(fā)
并行是指兩個或多個線程在同一時刻執(zhí)行,并發(fā)是指兩個或多個線程在 同一時間間隔 內發(fā)生潘拨。如果程序同時開啟的線程數小于CPU的核數吊输,那么不同進程的線程就可以分配給不同的CPU來運行,這就是并行铁追,如果線程數多于CPU的核數季蚂,那就需要并發(fā)技術。
二、Java多線程
Java虛擬機允許應用程序并發(fā)地運行多個執(zhí)行線程扭屁,常見的開啟新的線程的方法主要有4種算谈。
- (常用)任務類實現Runnable接口,在方法Run()里定義任務料滥。
public class Main {
public static void main(String[] args) {
//將ThreadNew實例作為參數實例化Thread之后start啟動線程
//Thread構造器接收Runnable接口實例
new Thread(new ThreadNew()).start();
System.out.println(" Thread Main ");
}
}
// 實現Runnable接口并在方法run里定義任務
class ThreadNew implements Runnable {
@Override
public void run() {
try { // 延時0.5秒
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New ");
}
}
- 任務類集成Thread然眼,重寫run()方法
public class Main {
public static void main(String[] args) {
new ThreadNew2().start();
System.out.println(" Thread Main ");
}
}
// 繼承自類Thread并重寫run方法
class ThreadNew2 extends Thread {
@Override
public void run() {
try { // 延時0.5秒
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New2 ");
}
}
- 實現接口Callable并在call()方法里得到線程執(zhí)行結果。
public class Main {
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(new ThreadNew3());
new Thread(futureTask).start();
System.out.println(" Thread Main ");
try {
System.out.println("執(zhí)行結果是 " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 實現接口Callable并在call()方法里定義任務
class ThreadNew3 implements Callable<String> {
@Override
public String call() throws Exception {
try { // 延時0.5秒
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New3 ");
return "Thread New3 Result";
}
}
- 通過線程池創(chuàng)建線程
上面4種就是Java中開啟新的線程的方式葵腹,其中第1種高每,實現Runnable接口最常用,也最靈活践宴,第2種鲸匿,因為任務類必須繼承自Thread,而Java中又僅支持單繼承阻肩,所以有時不太方便带欢,第3種方法主要是可以得到線程執(zhí)行的返回結果。
開啟的新線程都有一個線程優(yōu)先級烤惊,代表該線程的重要程度洪囤,可以通過Thread類的getPriority()和setPriority()來得到或者設置線程的優(yōu)先級。線程的優(yōu)先級范圍是1~10撕氧,默認情況下是5。
在線程創(chuàng)建完成還未啟動的時候喇完,我們可以通過方法setDaemon()來將線程設置為守護線程伦泥。守護線程,簡單理解為后臺運行線程锦溪,比如當程序運行時播放背景音樂。守護線程與普通線程在寫法上基本沒有區(qū)別,需要注意的是舵揭,當進程中所有非守護線程已經結束或者退出的時候如筛,即使還有守護線程在運行,進程仍然將結束则涯。
- 終止線程
Java沒有提供任何機制來安全地終止線程复局,那么怎么使線程停止或者中斷呢?
- 線程自己在run()方法執(zhí)行完后自動終止(安全的方式)
- 調用Thread.stop()方法強迫停止一個線程粟判,不過此方法是不安全的亿昏,已經不再建議使用。(不安全方式)
- 比較安全可靠的是利用Java的中斷機制档礁,使用方法Thread.interrupt()角钩。需要注意的是,通過中斷并不能直接終止另一個線程,需要被中斷的線程自己處理中斷递礼。被終止的線程一定要添加代碼對isInterrupted狀態(tài)進行處理惨险,否則即使代碼是死循環(huán)的情況下,線程也將永遠不會結束脊髓。(安全方式)
三辫愉、鎖機制
-
synchronized 同步鎖
synchronized,是Java里面的一個關鍵詞供炼,當它用來修飾一個方法或者一個代碼塊的時候一屋,能夠保證在同一時刻最多只有一個線程執(zhí)行該段代碼。用法如下:
寫法一袋哼、修飾在方法上
public synchronized void add1() {
}
寫法二冀墨、修飾在代碼塊上
public void add2() {
//這里的this指的是執(zhí)行這段代碼的對象
synchronized (this) {
}
}
寫法三、指定一個小的對象值進行加鎖
private byte[] lock = new byte[1];
public void add3() {
synchronized (lock) {
}
}
上面synchronized三種寫法中涛贯,最后一種性能和執(zhí)行效率最高诽嘉,synchronized修飾方法上的效率最低。原因主要是作用在方法體上的話弟翘,即使獲得了鎖那么進入方法體內分配資源還是需要一定時間的虫腋。前兩種鎖的對象都是對象本身,加鎖和釋放鎖都需要此對象的資源稀余,那么自己造一個byte對象悦冀,可以提升效率。
關于sychronized的詳細用法睛琳,可以查看這篇博文
-
ReentrantLock
在介紹ReentrantLock之前盒蟆,我們先看一個接口Lock。
Lock提供比synchronized更豐富师骗,更靈活的鎖操作历等。Lock的實現類比synchronized更靈活,但是必須手動釋放和開啟鎖辟癌,適用于代碼塊鎖寒屯,synchronized對象之間是互斥關系。
ReentrantLock是接口Lock的一個具體實現類黍少。當許多線程視圖訪問ReentrantLock保護的共享資源時寡夹,JVM將花費較少的時間來調度線程,用更多的時間執(zhí)行線程仍侥。它的用法主要如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock(); // 開啟鎖
try {
//方法體
} finally {
lock.unlock();//釋放鎖
}
}
}
-
volatile關鍵字
一旦一個共享變量(類的成員變量要出、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性农渊,即一個線程修改了某個變量的值患蹂,這新值對其他線程來說是立即可見的或颊。
2)禁止進行指令重排序。
下面我們看一下這個例子:
public class Counter {
public volatile static int count = 0;
public static void inc() {
//這里延遲1毫秒传于,使得結果明顯
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同時啟動1000個線程囱挑,去進行i++計算,看看實際結果
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
//這里每次運行的值都有可能不同,可能為1000
System.out.println("運行結果:Counter.count=" + Counter.count);
}
}
許多人認為加入volatile關鍵字之后沼溜,我們得到的最終值會是1000平挑,但實際上為Counter.count=992。
volatile的應用場景 https://blog.csdn.net/vking_wang/article/details/9982709
為什么會出現這種情況呢系草?
我們知道通熄,在jvm中,每一個線程運行時都有一個線程棧找都,線程棧保存了線程運行時候變量值信息唇辨。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值能耻,然后把堆內存變量的具體值load到線程本地內存中赏枚,建立一個變量副本,之后線程就不再和對象在堆內存變量值有任何關系晓猛,而是直接修改副本變量的值饿幅,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量戒职。這樣在堆中的對象的值就產生變化了栗恩。
這里可以用AtomicInteger來聲明count,它通過CAS算法保證了線程的安全性
read and load 從主存復制變量到當前工作內存
use and assign 執(zhí)行代碼洪燥,改變共享變量值
store and write 用工作內存數據刷新主存相關內容
但是在read load之后摄凡,如果主內存count變量發(fā)生修改之后,線程工作內存中的值由于已經加載蚓曼,不會產生對應的變化,所以計算出來的結果會和預期不一樣
四钦扭、線程池
Java通過Excutor提供4種線程池纫版,分別為:
- newCachedThreadPool創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要客情,可靈活回收空閑線程其弊,若無可回收,則新建線程膀斋。
- newFixedThreadPool 創(chuàng)建一個定長線程池梭伐,可控制線程最大并發(fā)數,超出的線程會在隊列中等待仰担。
- newScheduledThreadPool 創(chuàng)建一個定長線程池糊识,支持定時及周期性任務執(zhí)行。
- newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務赂苗,保證所有任務按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行愉耙。
1. newCachedThreadPool
創(chuàng)建一個可緩存(可擴展)線程池,如果線程長度超過處理需求拌滋,可靈活回收空閑線程朴沿,若無可回收的,則新建線程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
線程池為無限大败砂,當執(zhí)行第二個任務時第一個任務已經完成赌渣,會復用執(zhí)行第一個任務的線程,而不用每次新建線程昌犹。
從jconsole中坚芜,我們可以看到線程數后來在程序運行中維持不變
2. newFixedThreadPool
創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數祭隔,超出的線程會在隊列中等待
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
因為線程池大小為3货岭,每個任務輸出index后sleep 2秒,所以每兩秒打印3個數字疾渴。定長線程池的大小最好根據系統(tǒng)資源進行設置千贯。
3. newScheduledThreadPool
創(chuàng)建一個定長線程池,支持定時及周期性任務執(zhí)行搞坝。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延遲1秒后每3秒執(zhí)行一次搔谴。
ScheduledExecutorService比Timer更安全,功能更強大
4. newSingleThreadExecutor
創(chuàng)建一個單線程化的線程池桩撮,它只會用唯一的工作線程來執(zhí)行任務敦第,保證所有任務按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。示例代碼如下
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
結果依次輸出店量,相當于順序執(zhí)行各個任務芜果。
現行大多數GUI程序都是單線程的。Android中單線程可用于數據庫操作融师,文件操作右钾,應用批量安裝,應用批量刪除等不適合并發(fā)但可能IO阻塞性及影響UI線程響應的操作旱爆。
為什么要使用線程池:
- 減少了創(chuàng)建和銷毀線程的次數舀射,每個工作線程都可以被重復利用,可執(zhí)行多個任務怀伦。
- 可以根據系統(tǒng)的承受能力脆烟,調整線程池中工作線線程的數目,防止因為消耗過多的內存房待,而把服務器累趴下(每個線程需要大約1MB內存邢羔,線程開的越多驼抹,消耗的內存也就越大,最后死機)张抄。
Java里面線程池的頂級接口是Executor砂蔽,但是嚴格意義上講Executor并不是一個線程池,而只是一個執(zhí)行線程的工具署惯。真正的線程池接口是ExecutorService左驾。
比較重要的幾個類
ExecutorService: 真正的線程池接口。
ScheduledExecutorService: 能和Timer/TimerTask類似极谊,解決那些需要任務重復執(zhí)行的問題诡右。
傳統(tǒng)的timer的缺點:Timer對任務的調度是基于絕對時間的;所有的TimerTask只有一個線程TimerThread來執(zhí)行轻猖,因此同一時刻只有一個TimerTask在執(zhí)行帆吻;任何一個TimerTask的執(zhí)行異常都會導致Timer終止所有任務;由于基于絕對時間并且是單線程執(zhí)行咙边,因此在多個任務調度時猜煮,長時間執(zhí)行的任務被執(zhí)行后有可能導致短時間任務快速在短時間內被執(zhí)行多次或者干脆丟棄多個任務。
ThreadPoolExecutor: ExecutorService的默認實現败许。
ScheduledThreadPoolExecutor: 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現王带,周期性任務調度的類實現。