本文會介紹Java中多線程與併發的基礎,適合初學者食用,若是想看關於多線程與併發稍微進階一些的內容能夠看個人另外一篇博客— 《鎖》java
在計算機發展初期,每臺計算機是串行地執行任務的,若是碰上須要IO的地方,還須要等待長時間的用戶IO,後來通過一段時間有了批處理計算機,其能夠批量串行地處理用戶指令,但本質仍是串行,仍是不能併發執行。如何解決併發執行的問題呢?因而引入了進程的概念,每一個進程獨佔一分內存空間,進程是內存分配的最小單位,相互間運行互不干擾且能夠相互切換,如今咱們所看到的多個進程「同時"在運行,其實是進程高速切換的效果。算法
那麼有了線程以後,咱們的計算機系統看似已經很完美了,爲何還要進入線程呢?若是一個進程有多個子任務,每每一個進程須要逐個去執行這些子任務,但每每這些子任務是不相互依賴的,能夠併發執行,因此須要CPU進行更細粒度的切換。因此就引入了線程的概念,線程隸屬於某一個進程,它共享進程的內存資源,相互間切換更快速。編程
進程與線程的區別:緩存
1.進程是資源分配的最小單位,線程是CPU調度的最小單位。全部與進程相關的資源,均被記錄在PCB中。多線程
2.線程隸屬於某一個進程,共享所屬進程的資源。線程只由堆棧寄存器、程序計數器和TCB構成。併發
3.進程能夠看做獨立的應用,線程不能看做獨立的應用。app
4.進程有獨立的地址空間,相互不影響,而線程只是進程的不一樣執行路徑,若是線程掛了,進程也就掛了。因此多進程的程序比多線程程序健壯,可是切換消耗資源多。框架
Java中進程與線程的關係:ide
1.運行一個程序會產生一個進程,進程至少包含一個線程。函數
2.每一個進程對應一個JVM實例,多個線程共享JVM中的堆。
3.Java採用單線程編程模型,程序會自動建立主線程 。
4.主線程能夠建立子線程,原則上要後於子線程完成執行。
Java中建立線程的方式有兩種,無論使用繼承Thread的方式仍是實現Runnable接口的方式,都須要重寫run方法。調用start方法會建立一個新的線程並啓動,run方法只是啓動線程後的回調函數,若是調用run方法,那麼執行run方法的線程不會是新建立的線程,而若是使用start方法,那麼執行run方法的線程就是咱們剛剛啓動的那個線程。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new SubThread());
thread.run();
thread.start();
}
}
class SubThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("執行本方法的線程:"+Thread.currentThread().getName());
}
}
複製代碼
經過上述源碼圖,不難看出,Thread是一個類,而Runnable是一個接口,Runnable接口中只有一個沒有實現的run方法,能夠得知,Runnable並不能獨立開啓一個線程,而是依賴Thread類去建立線程,執行本身的run方法,去執行相應的業務邏輯,才能讓這個類具有多線程的特性。
public class Main extends Thread{
public static void main(String[] args) {
Main main = new Main();
main.start();
}
@Override
public void run() {
System.out.println("經過繼承Thread接口方式建立子線程成功,當前線程名:"+Thread.currentThread().getName());
}
}
複製代碼
運行結果:
public class Main{
public static void main(String[] args) {
SubThread subThread = new SubThread();
Thread thread = new Thread(subThread);
thread.start();
}
}
class SubThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("經過實現Runnable接口建立子線程成功,當前線程名:"+Thread.currentThread().getName());
}
}
複製代碼
運行結果:
public class Main{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("使用匿名內部類方式建立線程成功,當前線程名:"+Thread.currentThread().getName());
}
});
thread.start();
}
}
複製代碼
運行結果:
1.Thread是實現了Runnable接口的類,使得run支持多線程。2
2.因類的單一繼承原則,推薦使用Runnable接口,可使程序更加靈活。
經過剛纔的學習,咱們知道多線程的邏輯須要放到run方法中去執行,而run方法是沒有返回值的,那麼遇到須要返回值的情況就很差解決,那麼如何實現子線程返回值呢?
經過讓主線程等待,直到子線程運行完畢爲止。
實現方式:
public class Main{
static String str;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
str="子線程執行完畢";
}
});
thread.start();
//若是子線程還未對str進行賦值,則一直輪轉
while(str==null) {}
System.out.println(str);
}
}
複製代碼
join()方法能夠阻塞當前線程以等待子線程處理完畢。
實現方式:
public class Main{
static String str;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
str="子線程執行完畢";
}
});
thread.start();
//若是子線程還未對str進行賦值,則一直輪轉
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(str);
}
}
複製代碼
join方法能作到比主線程等待法更精準的控制,可是join方法的控制粒度並不夠細。好比,我須要控制子線程將字符串賦一個特定的值時,再執行主線程,這種操做join方法是沒有辦法作到的。
在JDK1.5以前,線程是沒有返回值的,一般程序猿須要獲取子線程返回值頗費周折,如今Java有了本身的返回值線程,即實現了Callable接口的線程,執行了實現Callable接口的線程以後,能夠得到一個Future對象,在該對象上調用一個get方法,就能夠執行子線程的邏輯並獲取返回的Object。
實現方式1(直接獲取 該方式爲錯誤方式):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是帶返回值的子線程";
return str;
}
public static void main(String[] args) {
Main main = new Main();
try {
String str = main.call();
/*這種方式爲何是錯誤方式? 和上文說的同樣,run()方法和start()方法的區別就在於 run()方法是線程啓動後的回調方法,若是直接調用,至關於沒有建立這個線程 仍是由主線程去執行。 因此這裏的call也同樣,若是直接調用call,並無子線程被建立, 而是至關於直接調用了類中的實例方法,獲取了返回值, 從頭至尾並無子線程的存在。*/
System.out.println(str);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
複製代碼
運行結果:
實現方式2(使用FutureTask):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是帶返回值的子線程";
return str;
}
public static void main(String[] args) {
FutureTask<String> task = new FutureTask<String>(new Main());
new Thread(task).start();
try {
if(!task.isDone()) {
System.out.println("任務沒有執行完成");
}
System.out.println("等待中...");
Thread.sleep(3000);
System.out.println(task.get());
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
複製代碼
運行結果:
實現方法3(使用線程池配合Future獲取):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是帶返回值的子線程";
return str;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService newCacheThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCacheThreadPool.submit(new Main());
if(!future.isDone()) {
System.out.println("線程還沒有執行結束");
}
System.out.println("等待中");
Thread.sleep(300);
System.out.println(future.get());
newCacheThreadPool.shutdown();
}
}
複製代碼
運行結果:
Java線程主要分爲如下六個狀態:新建態(new),運行態(Runnable),無限期等待(Waiting),限期等待(TimeWaiting),阻塞態(Blocked),結束(Terminated)。
新建態是線程處於已被建立但沒有被啓動的狀態,在該狀態下的線程只是被建立出來了,但並無開始執行其內部邏輯。
運行態分爲Ready和Running,當線程調用start方法後,並不會當即執行,而是去爭奪CPU,當線程沒有開始執行時,其狀態就是Ready,而當線程獲取CPU時間片後,從Ready態轉爲Running態。
處於等待狀態的線程不會自動甦醒,而只有等待被其它線程喚醒,在等待狀態中該線程不會被CPU分配時間,將一直被阻塞。如下操做會形成線程的等待:
1.沒有設置timeout參數的Object.wait()方法。
2.沒有設置timeout參數的Thread.join()方法。
3.LockSupport.park()方法(實際上park方法並非LockSupport提供的,而是在Unsafe中,LockSupport只是對其作了一層封裝,能夠看個人另外一篇博客《鎖》,裏面對於ReentrantLock的源碼解析有提到這個方法)。
處於限期等待的線程,CPU一樣不會分配時間片,但存在於限期等待的線程無需被其它線程顯式喚醒,而是在等待時間結束後,系統自動喚醒。如下操做會形成線程限時等待:
1.Thread.sleep()方法。
2.設置了timeout參數的Object.wait()方法。
3.設置了timeout參數的Thread.join()方法。
4.LockSupport.parkNanos()方法。
5.LockSupport.parkUntil()方法。
當多個線程進入同一塊共享區域時,例如Synchronized塊、ReentrantLock控制的區域等,會去整奪鎖,成功獲取鎖的線程繼續往下執行,而沒有獲取鎖的線程將進入阻塞狀態,等待獲取鎖。
已終止線程的線程狀態,線程已結束執行。
Sleep和Wait者兩個方法均可以使線程進入限期等待的狀態,那麼這兩個方法有什麼區別呢?
1.sleep方法由Thread提供,而wait方法由Object提供。
2.sleep方法能夠在任何地方使用,而wait方法只能在synchronized塊或synchronized方法中使用(由於必須獲wait方法會釋放鎖,只有獲取鎖了才能釋放鎖)。
3.sleep方法只會讓出CPU,不會釋放鎖,而wait方法不只會讓出CPU,還會釋放鎖。
測試代碼:
public class Main{
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA());
Thread threadB = new Thread(new ThreadB());
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static synchronized void print() {
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Sleep");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Wait");
try {
Main.class.wait(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");
}
}
class ThreadA implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
Main.print();
}
}
class ThreadB implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
Main.print();
}
}
複製代碼
執行結果:
從上面的結果能夠分析出:當線程A執行sleep後,等待一秒被喚醒後繼續持有鎖,執行以後的代碼,而執行wait以後,當即釋放了鎖,不只讓出了CPU還讓出了鎖,然後線程B當即持有鎖開始執行,和線程A執行了一樣的步驟,當線程B執行wait方法以後,釋放鎖,而後線程A拿到鎖打印了第一個執行完畢,而後線程B打印執行完畢。
notify能夠喚醒一個處於等待狀態的線程,上代碼:
public class Main{
public static void main(String[] args) {
Object lock = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
print();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
print();
lock.notify();
}
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static void print() {
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");
}
}
複製代碼
執行結果:
代碼解釋:線程A在開始執行時當即調用wait進入無限等待狀態,若是沒有別的線程來喚醒它,它將一直等待下去,因此此時B持有鎖開始執行,而且在執行完畢時調用了notify方法,該方法能夠喚醒wait狀態的A線程,因而A線程甦醒,開始執行剩下的代碼。
notifyAll能夠用於喚醒全部等待的線程,使全部處於等待狀態的線程都變爲ready狀態,去從新爭奪鎖。
public class Main{
public static void main(String[] args) {
Object lock = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
print();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
print();
lock.notifyAll();
}
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static void print() {
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");
}
}
複製代碼
執行結果:
要喚醒前一個例子中的線程A,不光notify方法能夠作到,調用notifyAll方法一樣也能夠作到,那麼二者有什麼區別呢?
要說清楚他們的區別,首先要簡單的說一下Java synchronized的一些原理,在openjdk中查看java的源碼能夠看到,java對象中存在monitor鎖,monitor對象中包含鎖池和等待池(這部分的詳細內容在另外一篇文章《鎖》中有詳細介紹,這裏就簡單說一說)
鎖池,假設有多個對象進入synchronized塊爭奪鎖,而此時已經有一個對象獲取到了鎖,那麼剩餘爭奪鎖的對象將直接進入鎖池中。
等待池,假設某個線程調用了對象的wait方法,那麼這個線程將直接進入等待池,而等待池中的對象不會去爭奪鎖,而是等待被喚醒。
下面能夠說notify和notifyAll的區別了:
notifyAll會讓全部處於等待池中的線程所有進入鎖池去爭奪鎖,而notify只會隨機讓其中一個線程去爭奪鎖。
/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this * hint. * * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control constructs such as the ones in the * {@link java.util.concurrent.locks} package. */
public static native void yield();
複製代碼
yield源碼上有一段長長的註釋,其大意是說:當前線程調用yield方法時,會給當前線程調度器一個暗示,當前線程願意讓出CPU的使用,可是它的做用應結合詳細的分析和測試來確保已經達到了預期的效果,由於調度器可能會無視這個暗示,使用這個方法是不那麼合適的,或許在測試環境中使用它會比較好。
測試:
public class Main{
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadA正在執行yield");
Thread.yield();
System.out.println("ThreadA執行yield方法完成");
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadB正在執行yield");
Thread.yield();
System.out.println("ThreadB執行yield方法完成");
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
複製代碼
測試結果:
能夠看出,存在不一樣的測試結果,這裏選出兩張。
第一種結果:線程A執行完yield方法,讓出cpu給線程B執行。而後兩個線程繼續執行剩下的代碼。
第二種結果:線程A執行yield方法,讓出cpu給線程B執行,可是線程B執行yield方法後並無讓出cpu,而是繼續往下執行,此時就是系統無視了這個暗示。
interrupt函數能夠中斷一個線程,在interrupt以前,一般使用stop方法來終止一個線程,可是stop方法過於暴力,它的特色是,不論被中斷的線程以前處於一個什麼樣的狀態,都無條件中斷,這會致使被中斷的線程後續的一些清理工做沒法順利完成,引起一些沒必要要的異常和隱患,還有可能引起數據不一樣步的問題。
interrupt方法的原理與stop方法相比就顯得溫柔的多,當調用interrupt方法去終止一個線程時,它並不會暴力地強制終止線程,而是通知這個線程應該要被中斷了,和yield同樣,這也是一種暗示,至因而否應該中斷,由被中斷的線程本身去決定。當對一個線程調用interrupt方法時:
1.若是該線程處於被阻塞狀態,則當即退出阻塞狀態,拋出InterruptedException異常。
2.若是該線程處於running狀態,則將該線程的中斷標誌位設置爲true,被設置的線程繼續運行,不受影響,當運行結束時由線程決定是否被中斷。
線程池的引入是用來解決在平常開發的多線程開發中,若是開發者須要使用到很是多的線程,那麼這些線程在被頻繁的建立和銷燬時,會對系統形成必定的影響,有可能系統在建立和銷燬這些線程所耗費的時間會比完成實際需求的時間還要長。另外,在線程不少的情況下,對線程的管理就造成了一個很大的問題,開發者一般要將注意力從功能上轉移到對雜亂無章的線程進行管理上,這項動做其實是很是耗費精力的。
指定工做線程數量的線程池。
處理大量中斷事件工做任務的線程池,
1.試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程。
2.若是線程閒置的時間超過閾值,則會被終止並移出緩存。
3.系統長時間閒置的時候,不會消耗什麼資源。
建立惟一的工做線程來執行任務,若是線程異常結束,會有另外一個線程取代它。可保證順序執行任務。
定時或週期性工做調度,二者的區別在於前者是單一工做線程,後者是多線程
內部構建ForkJoinPool,利用working-stealing算法,並行地處理任務,不保證處理順序。
**Fork/Join框架:**把大任務分割稱若干個小任務並行執行,最終彙總每一個小任務後獲得大任務結果的框架。
線程是稀缺資源,若是無限制地建立線程,會消耗系統資源,而線程池能夠代替開發者管理線程,一個線程在結束運行後,不會銷燬線程,而是將線程歸還線程池,由線程池再進行管理,這樣就能夠對線程進行復用。
因此線程池不但能夠下降資源的消耗,還能夠提升線程的可管理性。
public class Main{
public static void main(String[] args) {
ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10);
newFixThreadPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("經過線程池啓動線程成功");
}
});
newFixThreadPool.shutdown();
}
}
複製代碼
要知道這個點首先要先說說ThreadPoolExecutor的構造函數,其中有幾個參數:
1.corePoolSize:核心線程數量。
2.maximumPoolSize:線程不夠用時能建立的最大線程數。
3.workQueue:等待隊列。
那麼新任務提交後會執行下列判斷:
1.若是運行的線程少於corePoolSize,則建立新線程來處理任務,即時線程池中的其它線程是空閒的。
2.若是線程池中的數量大於等於corePoolSize且小於maximumPoolSize,則只有當workQueue滿時,才建立新的線程去處理任務。
3.若是設置的corePoolSize和maximumPoolSize相同,則建立的線程池大小是固定的,若是此時有新任務提交,若workQueue未滿,則放入workQueue,等待被處理。
4.若是運行的線程數大於等於maximumPoolSize,maximumPoolSize,這時若是workQueue已經滿了,則經過handler所指定的策略來處理任務。
AbortPolicy:直接拋出異常,默認。
CallerRunsPolicy:用調用者所在的線程來執行任務。
DiscardOldestPolicy:丟棄隊列中靠最前的任務,並執行當前任務。
DiscardPolicy:直接丟棄任務
自定義。
這個問題並非什麼祕密,在網上各大技術網站均有文章說明,我就拿一個最受承認的寫上吧
CPU密集型:線程數 = 核心數或者核心數+1
IO密集型:線程數 = CPU核數*(1+平均等待時間/平均工做時間)
固然這個也不能徹底依賴這個公式,更多的是要依賴平時的經驗來操做,這個公式也只是僅供參考而已。
本文提供了一些Java多線程和併發方面最最基礎的知識,適合初學者瞭解Java多線程的一些基本知識,若是想了解更多的關於併發方面的內容能夠看個人另外一篇博客 《鎖》。
歡迎你們訪問個人我的博客:Object's Blog