非常詳細的JAVA併發基本線程機制
編程問題中相當大的一部分都可以通過使用順序編程來解決。然而對於某些問題,若果能夠併發地執行程序中的多個部分,則會變得非常方便甚至非常必要,因爲這些部分要麼看起來在併發執行,要麼在多處理器環境下可以同時執行。
JAVA是一種多線程語言,並提出了併發問題。實現併發最直接的方式是在操作系統級別使用進程。進程是運行在它自己的地址空間內的自包容程序。多任務操作系統可以通過週期性地將CPU從一個進程切換到另一個進程,來實現同時運行多個進程。每個任務都作爲進程在其自己的地址空間中執行,因此它們是完全獨立的;更重要的事,對進程來說,它們之間沒有任何彼此通信的需要,因爲它們是完全獨立的。
JAVA在併發上採取了比較傳統的方式,在順序語言的基礎上提供對線程的支持。線程機制是在由執行程序表示的單一進程中創建任務。
在但CPU機器上使用多任務的程序在任意時刻仍舊值在執行一項工作,因此從理論上講,肯定可以不用任何很污二編寫出相同的程序。但是毛病發提供了一個重要的組織結構上的好處:你的程序設計可以極大地簡化。而例如仿真,沒有併發數的支持是很難解決的。
JAVA的線程機制是搶佔式的,這表示調度機制會週期性地中斷線程將上下文切換到另一個線程,從而爲每個線程都提供時間片,是的每個線程都會分配到數量合理的時間去驅動它的任務。
併發編程使我們可以將程序劃分爲多個分離的、獨立運行的任務。通過使用多線程機制,這些獨立任務(子任務)中的每一個都將有執行線程來驅動。一個線程就是在進程中的一個單一的順序控制流,因此,單個進程可以擁有多個併發執行的任務,但是你的程序使得每個任務都好像有其自己的CPU一樣,其底層機制是切分CPU時間(有需要了解這部分知識的朋友可以在下方留言,可以出一篇詳細的博客)。
CPU輪流給每個任務分配其佔用時。每個任務都覺得自己再一直在佔用CPU,大三事實上CPU時間是劃分成片段分配給了所有的任務(例外是程序確實運行在多個CPU之上)。線程的一大好處是可以使你從這個層次抽身出來,即代碼不必知道它是運行在具有一個還是多個CPU的機器上。所以,使用線程機制是一種建立透明的、可擴展的程序的方法。
JAVA多線程機制的代碼介紹篇章(實用篇)
1. 定義任務
package Kim_1;
/*定義任務,需要一種描述任務的方式,可由Runnable接口提供。
* 想要定義任務,只需要實現Runnable接口並編寫run()方法,使得該任務可以執行你的命令
* */
public class LiftOff implements Runnable{
protected int countDown = 10;// Default
private static int taskcount = 0;// 記任務數
private final int id = taskcount++;
public LiftOff(){}
public LiftOff(int countDown) {
this.countDown = countDown;
}
//
public String status() {
return "#"+id+"("+(countDown > 0 ? countDown:"LiftOff!")+").";
}
public void run() {
while(countDown-->0) {
System.out.println(status());
// Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程;
Thread.yield();
}
}
}
MAIN類:
package Kim_1;
public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
}
}
猜猜運行結果是什麼勒?
分析對了嗎?沒對沒關係,跟我繼續往下,你就能漸漸理解了。
從結果我們可以得知,整個過程我們只有一個過程產生了ListOff對象,即目前只有0號一個LiftOff任務被執行;後面括號中的倒數進行發射爲描述的任務。
不過就納悶了說好的多線程呢?
的確,如果這裏我們需要實現多線程的話,還少了一些步驟;因爲雖然我們實現了Runnable接口,並重寫了run()函數,但是這個方法並無特殊之處——它不會產生任何內在的線程能力。要實現線程行爲,你必須顯示地講一個任務附着到線程上。
通過上面的程序我們可以再來理一遍上面的程序-任務-進程圖
(理解這個模式,之後就能很簡單的區分繼承自Thread和實現Runnable接口的區別了)
2. Thread類
將Runnable對象轉變爲工作任務的傳統方式是把它提交給一個Thread構造器。
下面的例子展示瞭如何使用Thread來驅動LiftOff對象:
package Kim_1;
public class BasicThreads {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff());
t.start();
System.out.println("Waiting for LiftOff");
}
}
運行結果如下:
Thread構造器只需要一個Runnable對象。調用Thread對象的start()方法爲該線程執行必要的初始化操作,然後調用Runnble的run()方法,以便在這個新線程中啓動該任務。初學者看到這樣的結果可能會覺得很詫異,比較平時習慣了順序執行的方式;但這也就是多線程的魅力所在,實際上t.start()產生的是對LiftOff.run()的方法調用,並且這個方法還沒有完成,但是因爲LiftOff.run()是由不同的線程執行的,因此你人就可以執行main()線程中的其他操作。
到這裏不可避免還是一頭霧水,我們暫時先了解有這麼個東西,能運行出這樣奇怪的結果,具體細節咱們後面慢慢道來。
有了上面的基礎,你可以像方一樣很容易地添加更多的線程去驅動更多的任務,結果不方便截圖,各位小夥伴可以在自己的PC上運行一下試試,可以顯然的看到運行的結果雜亂無章,並且每次運行的結果幾乎都不一樣,這說明不同任務的執行在線程中被換進換出時混在了一起,這種交換是由線程條肚臍自動控制的,且線程調度機制是非確定性的。
(注意:本例子中,單一線程(main()在創建所以的LiftOff線程,所以id爲0-4。但是如果多個線程在創建LiftOff線程,那麼就要可能會有多個LiftOff擁有相同的id))
package Kim_1;
public class MoreBasicThreads {
public static void main(String[] args) {
for(int i=0;i<5;i++)
new Thread(new LiftOff()).start();
System.out.println("Waiting for LiftOff");
}
}
當main()創建Thread對象時,它並可以捕獲任何對這些對象的引用。而每個Thread都“註冊”了它自己,因此有一個對它的引用,而且在它的任務結束退出其run()並死亡之前,垃圾回收期無法清除它。
3. 使用Executor(執行器)
Executor可以爲我們管理Thread對象,從而簡化併發編程;它在客戶端和任務之間提供了一個間接層;它允許我們管理一部任務的執行,而無需顯示地管理線程的聲明週期。
我們可以使用Excutor來代替MoreBasicThread.java中顯示地創建Thread對象。LiftOff對象知道如何運行具體的任務。ExecutorService知道如何構建恰當的上下文來執行Runnable對象。
package Kim_1;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();// ExecutorService是具有服務生命週期的Executor
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();// 可以防止新任務被提交給這個Executor
}
}
這一次我們可以發現運行的結果齊整多了。
4. 從任務中產生返回值
package Kim_1;
/*從人物中產生返回值,
* 通過實現Callable接口,Callable是一種具有類型參數的泛型,它的類型參數表示從方法call()中返回的值的類型
* 並且必須使用ExcecutorService.submit()方法調用它*/
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id) {
this.id = id;
}
public String call() {
return "result of TaskWithResult" + id;
}
}
public class CallableDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> result = new ArrayList<Future<String>>();
for (int i = 0; i < 10; i++) {
result.add(exec.submit(new TaskWithResult(i)));
}
for (Future<String> fs : result) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
// TODO: handle exception
System.out.println(e);
return;
}catch (ExecutionException e) {
// TODO: handle exception
System.out.println(e);
}finally {
exec.shutdown();
}
}
}
}
5. 休眠
package Kim_1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class SleepingTask extends LiftOff{
public void run() {
try {
while (countDown-->0) {
System.out.print(status());
TimeUnit.MILLISECONDS.sleep(1000);
}
} catch (Exception e) {
// TODO: handle exception
System.err.println("Interrupted");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new SleepingTask());
}
exec.shutdown();
}
}
6. 優先級
線程的優先級將該線程的重要性傳給了調度器。調度器會傾向於讓優先級最高的線程限執行;但這並不意味着優先級低的線程得不到執行,優先級不會造成死鎖現象。
三個常用的優先級級別常量:MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY
package Kim_1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
* */
public class SimplePriorities implements Runnable{
private int countDown = 5;
private volatile double d;
private int priority;
public SimplePriorities(int priority){
this.priority = priority;
}
//toString()方法被覆蓋,用來打印線程的名字
//這裏爲自動生成的名稱(你也可以在構造器裏自己設置這個名稱)
public String toString(){
return Thread.currentThread()+":"+countDown;
}
public void run() {
Thread.currentThread().setPriority(priority);
while(true) {
for (int i = 0; i < 100000; i++) {
d+=(Math.PI + Math.E)/(double)i;
if (i%1000==0) {
Thread.yield();
}
System.out.println(this);
if (--countDown==0) {
return;
}
}
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
exec.shutdown();
}
}
7.讓步
yield()方法。當已經知道run()方法的循環的一次迭代過程中所需要的工作,就可以將線程調度機制一個暗示:你的工作已經做得差不多了,可以讓別的線程來使用CPU了。這個暗示將通過調用yield()方法來做出。當調用yield()時,你也是在建議具有相同優先級瑟其他線程可以運行。
注意:雖然yield()可以對線程的產生分佈給予了良好的處理機制,但是對於任何重要的控制或在調整應用時,都不能依賴於yield();
8.後臺線程
後臺線程是指在程序運行時在後臺提供的一種通用服務線程,並且這種線程並不屬於程序中不可或缺的部分,即當所有的非後臺程序結束時,程序也就終止了,同時會殺死進程中的所以後臺線程。比如:執行main()的就是一個非後臺線程。
必須在線程啓動之前調用setDaemon()方法,才能把它設置爲後臺線程。實例中因爲main()線程別設定爲短暫睡嗎,所以可以觀察到所以後臺線程啓動後的結果。
//: concurrency/SimpleDaemons.java
// Daemon threads don’t prevent the program from ending.
package Kim_1;
import java.util.concurrent.TimeUnit;
public class SimpleDaemons implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.print(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
System.out.print("sleep() interrupted");
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
// 將daemon設爲後臺線程
daemon.setDaemon(true); // Must call before start()
daemon.start();
}
System.out.print("All daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
} /* Output: (Sample)
All daemons started
Thread[Thread-0,5,main] SimpleDaemons@530daa
Thread[Thread-1,5,main] SimpleDaemons@a62fc3
Thread[Thread-2,5,main] SimpleDaemons@89ae9e
Thread[Thread-3,5,main] SimpleDaemons@1270b73
Thread[Thread-4,5,main] SimpleDaemons@60aeb0
Thread[Thread-5,5,main] SimpleDaemons@16caf43
Thread[Thread-6,5,main] SimpleDaemons@66848c
Thread[Thread-7,5,main] SimpleDaemons@8813f2
Thread[Thread-8,5,main] SimpleDaemons@1d58aae
Thread[Thread-9,5,main] SimpleDaemons@83cc67
...
*///:~
9.加入一個線程
一個線程可以在其他線程智商調用join()方法,其效果十等待一段時間知道第二個線程結束才能繼續執行。如果某個線程在另一個線程t上調用t.join(),此線程將被掛起,直到目標線程t結束才恢復(t.isAlive()爲false)。join()方法的調用可以被中斷,做法是在調用線程上調用interrupt()方法(追interrupt需要處理異常,用到try-catch語句塊)
從下方運行結果我們可以看出,Sleeper被中斷或者是正常結束,Joiner將和Sleeper一同結束。
//: concurrency/Joining.java
// Understanding join().
package Kim_1;
class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch(InterruptedException e) {
System.out.println(getName() + " was interrupted. " +
"isInterrupted(): " + isInterrupted());
return;
}
System.out.println(getName() + " has awakened");
}
}
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
public class Joining {
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
} /* Output:
Grumpy was interrupted. isInterrupted(): false
Doc join completed
Sleepy has awakened
Dopey join completed
*///:~