国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術(shù)|正則表達(dá)式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務(wù)器之家 - 編程語言 - Java教程 - Java線程的基本概念

Java線程的基本概念

2020-08-10 19:19老馬說編程 Java教程

本文主要介紹了Java線程的基本概念。具有很好的參考價值,下面跟著小編一起來看下吧

在之前的章節(jié)中,我們都是假設(shè)程序中只有一條執(zhí)行流,程序從main方法的第一條語句逐條執(zhí)行直到結(jié)束。從本節(jié)開始,我們討論并發(fā),在程序中創(chuàng)建線程來啟動多條執(zhí)行流,并發(fā)和線程是一個復(fù)雜的話題,本節(jié),我們先來討論Java中線程的一些基本概念。

創(chuàng)建線程

線程表示一條單獨的執(zhí)行流,它有自己的程序執(zhí)行計數(shù)器,有自己的棧。下面,我們通過創(chuàng)建線程來對線程建立一個直觀感受,在Java中創(chuàng)建線程有兩種方式,一種是繼承Thread,另外一種是實現(xiàn)Runnable接口,我們先來看第一種。

繼承Thread

Java中java.lang.Thread這個類表示線程,一個類可以繼承Thread并重寫其run方法來實現(xiàn)一個線程,如下所示:

?
1
2
3
4
5
6
public class HelloThread extends Thread {
 @Override
 public void run() {
 System.out.println("hello");
 }
}

HelloThread這個類繼承了Thread,并重寫了run方法。run方法的方法簽名是固定的,public,沒有參數(shù),沒有返回值,不能拋出受檢異常。run方法類似于單線程程序中的main方法,線程從run方法的第一條語句開始執(zhí)行直到結(jié)束。

定義了這個類不代表代碼就會開始執(zhí)行,線程需要被啟動,啟動需要先創(chuàng)建一個HelloThread對象,然后調(diào)用Thread的start方法,如下所示:

?
1
2
3
4
public static void main(String[] args) {
 Thread thread = new HelloThread();
 thread.start();
}

我們在main方法中創(chuàng)建了一個線程對象,并調(diào)用了其start方法,調(diào)用start方法后,HelloThread的run方法就會開始執(zhí)行,屏幕輸出:

hello

為什么調(diào)用的是start,執(zhí)行的卻是run方法呢?start表示啟動該線程,使其成為一條單獨的執(zhí)行流,背后,操作系統(tǒng)會分配線程相關(guān)的資源,每個線程會有單獨的程序執(zhí)行計數(shù)器和棧,操作系統(tǒng)會把這個線程作為一個獨立的個體進(jìn)行調(diào)度,分配時間片讓它執(zhí)行,執(zhí)行的起點就是run方法。

如果不調(diào)用start,而直接調(diào)用run方法呢?屏幕的輸出并不會發(fā)生變化,但并不會啟動一條單獨的執(zhí)行流,run方法的代碼依然是在main線程中執(zhí)行的,run方法只是main方法調(diào)用的一個普通方法。

怎么確認(rèn)代碼是在哪個線程中執(zhí)行的呢?Thread有一個靜態(tài)方法currentThread,返回當(dāng)前執(zhí)行的線程對象:

public static native Thread currentThread();

每個Thread都有一個id和name:

?
1
2
public long getId()
public final String getName()

這樣,我們就可以判斷代碼是在哪個線程中執(zhí)行的,我們在HelloThead的run方法中加一些代碼:

?
1
2
3
4
5
@Override
public void run() {
 System.out.println("thread name: "+ Thread.currentThread().getName());
 System.out.println("hello");
}

如果在main方法中通過start方法啟動線程,程序輸出為:

?
1
2
thread name: Thread-0
hello

如果在main方法中直接調(diào)用run方法,程序輸出為:

?
1
2
thread name: main
hello

調(diào)用start后,就有了兩條執(zhí)行流,新的一條執(zhí)行run方法,舊的一條繼續(xù)執(zhí)行main方法,兩條執(zhí)行流并發(fā)執(zhí)行,操作系統(tǒng)負(fù)責(zé)調(diào)度,在單CPU的機(jī)器上,同一時刻只能有一個線程在執(zhí)行,在多CPU的機(jī)器上,同一時刻可以有多個線程同時執(zhí)行,但操作系統(tǒng)給我們屏蔽了這種差異,給程序員的感覺就是多個線程并發(fā)執(zhí)行,但哪條語句先執(zhí)行哪條后執(zhí)行是不一定的。當(dāng)所有線程都執(zhí)行完畢的時候,程序退出。

實現(xiàn)Runnable接口

通過繼承Thread來實現(xiàn)線程雖然比較簡單,但我們知道,Java中只支持單繼承,每個類最多只能有一個父類,如果類已經(jīng)有父類了,就不能再繼承Thread,這時,可以通過實現(xiàn)java.lang.Runnable接口來實現(xiàn)線程。

Runnable接口的定義很簡單,只有一個run方法,如下所示:

?
1
2
3
public interface Runnable {
 public abstract void run();
}

一個類可以實現(xiàn)該接口,并實現(xiàn)run方法,如下所示:

?
1
2
3
4
5
6
public class HelloRunnable implements Runnable {
 @Override
 public void run() {
  System.out.println("hello");
 }
}

僅僅實現(xiàn)Runnable是不夠的,要啟動線程,還是要創(chuàng)建一個Thread對象,但傳遞一個Runnable對象,如下所示:

?
1
2
3
4
public static void main(String[] args) {
 Thread helloThread = new Thread(new HelloRunnable());
 helloThread.start();
}

無論是通過繼承Thead還是實現(xiàn)Runnable接口來實現(xiàn)線程,啟動線程都是調(diào)用Thread對象的start方法。

線程的基本屬性和方法

id和name

前面我們提到,每個線程都有一個id和name,id是一個遞增的整數(shù),每創(chuàng)建一個線程就加一,name的默認(rèn)值是"Thread-"后跟一個編號,name可以在Thread的構(gòu)造方法中進(jìn)行指定,也可以通過setName方法進(jìn)行設(shè)置,給Thread設(shè)置一個友好的名字,可以方便調(diào)試。

優(yōu)先級

線程有一個優(yōu)先級的概念,在Java中,優(yōu)先級從1到10,默認(rèn)為5,相關(guān)方法是:

?
1
2
public final void setPriority(int newPriority)
public final int getPriority()

這個優(yōu)先級會被映射到操作系統(tǒng)中線程的優(yōu)先級,不過,因為操作系統(tǒng)各不相同,不一定都是10個優(yōu)先級,Java中不同的優(yōu)先級可能會被映射到操作系統(tǒng)中相同的優(yōu)先級,另外,優(yōu)先級對操作系統(tǒng)而言更多的是一種建議和提示,而非強(qiáng)制,簡單的說,在編程中,不要過于依賴優(yōu)先級。

狀態(tài)

線程有一個狀態(tài)的概念,Thread有一個方法用于獲取線程的狀態(tài):

public State getState()

返回值類型為Thread.State,它是一個枚舉類型,有如下值:

?
1
2
3
4
5
6
7
8
public enum State {
 NEW,
 RUNNABLE,
 BLOCKED,
 WAITING,
 TIMED_WAITING,
 TERMINATED;
}

關(guān)于這些狀態(tài),我們簡單解釋下:

  • NEW: 沒有調(diào)用start的線程狀態(tài)為NEW
  • TERMINATED: 線程運行結(jié)束后狀態(tài)為TERMINATED
  • RUNNABLE: 調(diào)用start后線程在執(zhí)行run方法且沒有阻塞時狀態(tài)為RUNNABLE,不過,RUNNABLE不代表CPU一定在執(zhí)行該線程的代碼,可能正在執(zhí)行也可能在等待操作系統(tǒng)分配時間片,只是它沒有在等待其他條件
  • BLOCKED、WAITING、TIMED_WAITING:都表示線程被阻塞了,在等待一些條件,其中的區(qū)別我們在后續(xù)章節(jié)再介紹

Thread還有一個方法,返回線程是否活著:

public final native boolean isAlive()

線程被啟動后,run方法運行結(jié)束前,返回值都是true。

是否daemo線程

Thread有一個是否daemo線程的屬性,相關(guān)方法是:

?
1
2
public final void setDaemon(boolean on)
public final boolean isDaemon()

前面我們提到,啟動線程會啟動一條單獨的執(zhí)行流,整個程序只有在所有線程都結(jié)束的時候才退出,但daemo線程是例外,當(dāng)整個程序中剩下的都是daemo線程的時候,程序就會退出。

daemo線程有什么用呢?它一般是其他線程的輔助線程,在它輔助的主線程退出的時候,它就沒有存在的意義了。在我們運行一個即使最簡單的"hello world"類型的程序時,實際上,Java也會創(chuàng)建多個線程,除了main線程外,至少還有一個負(fù)責(zé)垃圾回收的線程,這個線程就是daemo線程,在main線程結(jié)束的時候,垃圾回收線程也會退出。

sleep方法

Thread有一個靜態(tài)的sleep方法,調(diào)用該方法會讓當(dāng)前線程睡眠指定的時間,單位是毫秒:

public static native void sleep(long millis) throws InterruptedException;

睡眠期間,該線程會讓出CPU,但睡眠的時間不一定是確切的給定毫秒數(shù),可能有一定的偏差,偏差與系統(tǒng)定時器和操作系統(tǒng)調(diào)度器的準(zhǔn)確度和精度有關(guān)。

睡眠期間,線程可以被中斷,如果被中斷,sleep會拋出InterruptedException,關(guān)于中斷以及中斷處理,我們后續(xù)章節(jié)再介紹。

yield方法

Thread還有一個讓出CPU的方法:

public static native void yield();

這也是一個靜態(tài)方法,調(diào)用該方法,是告訴操作系統(tǒng)的調(diào)度器,我現(xiàn)在不著急占用CPU,你可以先讓其他線程運行。不過,這對調(diào)度器也僅僅是建議,調(diào)度器如何處理是不一定的,它可能完全忽略該調(diào)用。

join方法

在前面HelloThread的例子中,HelloThread沒執(zhí)行完,main線程可能就執(zhí)行完了,Thread有一個join方法,可以讓調(diào)用join的線程等待該線程結(jié)束,join方法的聲明為:

public final void join() throws InterruptedException

在等待線程結(jié)束的過程中,這個等待可能被中斷,如果被中斷,會拋出InterruptedException。

join方法還有一個變體,可以限定等待的最長時間,單位為毫秒,如果為0,表示無期限等待:

public final synchronized void join(long millis) throws InterruptedException

在前面的HelloThread示例中,如果希望main線程在子線程結(jié)束后再退出,main方法可以改為:

?
1
2
3
4
5
public static void main(String[] args) throws InterruptedException {
 Thread thread = new HelloThread();
 thread.start();
 thread.join();
}

過時方法

Thread類中還有一些看上去可以控制線程生命周期的方法,如:

?
1
2
3
public final void stop()
public final void suspend()
public final void resume()

這些方法因為各種原因已被標(biāo)記為了過時,我們不應(yīng)該在程序中使用它們。

共享內(nèi)存及問題

共享內(nèi)存

前面我們提到,每個線程表示一條單獨的執(zhí)行流,有自己的程序計數(shù)器,有自己的棧,但線程之間可以共享內(nèi)存,它們可以訪問和操作相同的對象。我們看個例子,代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ShareMemoryDemo {
 private static int shared = 0;
 private static void incrShared(){
  shared ++;
 }
 static class ChildThread extends Thread {
  List<String> list;
  
  public ChildThread(List<String> list) {
   this.list = list;
  }
  @Override
  public void run() {
   incrShared();
   list.add(Thread.currentThread().getName());
  }
 }
 public static void main(String[] args) throws InterruptedException {
  List<String> list = new ArrayList<String>();
  Thread t1 = new ChildThread(list);
  Thread t2 = new ChildThread(list);
  t1.start();
  t2.start();
  t1.join();
  t2.join();
  System.out.println(shared);
  System.out.println(list);
 }
}

在代碼中,定義了一個靜態(tài)變量shared和靜態(tài)內(nèi)部類ChildThread,在main方法中,創(chuàng)建并啟動了兩個ChildThread對象,傳遞了相同的list對象,ChildThread的run方法訪問了共享的變量shared和list,main方法最后輸出了共享的shared和list的值,大部分情況下,會輸出期望的值:

[Thread-0, Thread-1]

通過這個例子,我們想強(qiáng)調(diào)說明執(zhí)行流、內(nèi)存和程序代碼之間的關(guān)系。

該例中有三條執(zhí)行流,一條執(zhí)行main方法,另外兩條執(zhí)行ChildThread的run方法。

  • 不同執(zhí)行流可以訪問和操作相同的變量,如本例中的shared和list變量。
  • 不同執(zhí)行流可以執(zhí)行相同的程序代碼,如本例中incrShared方法,ChildThread的run方法,被兩條ChildThread執(zhí)行流執(zhí)行,incrShared方法是在外部定義的,但被ChildThread的執(zhí)行流執(zhí)行,在分析代碼執(zhí)行過程時,理解代碼在被哪個線程執(zhí)行是很重要的。
  • 當(dāng)多條執(zhí)行流執(zhí)行相同的程序代碼時,每條執(zhí)行流都有單獨的棧,方法中的參數(shù)和局部變量都有自己的一份。

當(dāng)多條執(zhí)行流可以操作相同的變量時,可能會出現(xiàn)一些意料之外的結(jié)果,我們來看下。

競態(tài)條件

所謂競態(tài)條件(race condition)是指,當(dāng)多個線程訪問和操作同一個對象時,最終執(zhí)行結(jié)果與執(zhí)行時序有關(guān),可能正確也可能不正確,我們看一個例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CounterThread extends Thread {
 private static int counter = 0;
 @Override
 public void run() {
  try {
   Thread.sleep((int)(Math.random()*100));
  } catch (InterruptedException e) {
  }
  counter ++;
 }
 public static void main(String[] args) throws InterruptedException {
  int num = 1000;
  Thread[] threads = new Thread[num];
  for(int i=0; i<num; i++){
   threads[i] = new CounterThread();
   threads[i].start();
  }
  for(int i=0; i<num; i++){
   threads[i].join();
  }
  System.out.println(counter);
 }
}

這段代碼容易理解,有一個共享靜態(tài)變量counter,初始值為0,在main方法中創(chuàng)建了1000個線程,每個線程就是隨機(jī)睡一會,然后對counter加1,main線程等待所有線程結(jié)束后輸出counter的值。

期望的結(jié)果是1000,但實際執(zhí)行,發(fā)現(xiàn)每次輸出的結(jié)果都不一樣,一般都不是1000,經(jīng)常是900多。為什么會這樣呢?因為counter++這個操作不是原子操作,它分為三個步驟:

  • 取counter的當(dāng)前值
  • 在當(dāng)前值基礎(chǔ)上加1
  • 將新值重新賦值給counter

兩個線程可能同時執(zhí)行第一步,取到了相同的counter值,比如都取到了100,第一個線程執(zhí)行完后counter變?yōu)?01,而第二個線程執(zhí)行完后還是101,最終的結(jié)果就與期望不符。

怎么解決這個問題呢?有多種方法:

  • 使用synchronized關(guān)鍵字
  • 使用顯式鎖
  • 使用原子變量

關(guān)于這些方法,我們在后續(xù)章節(jié)再介紹。

內(nèi)存可見性

多個線程可以共享訪問和操作相同的變量,但一個線程對一個共享變量的修改,另一個線程不一定馬上就能看到,甚至永遠(yuǎn)也看不到,這可能有悖直覺,我們來看一個例子。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class VisibilityDemo {
 private static boolean shutdown = false;
 static class HelloThread extends Thread {
  @Override
  public void run() {
   while(!shutdown){
    // do nothing
   }
   System.out.println("exit hello");
  }
 }
 public static void main(String[] args) throws InterruptedException {
  new HelloThread().start();
  Thread.sleep(1000);
  shutdown = true;
  System.out.println("exit main");
 }
}

在這個程序中,有一個共享的boolean變量shutdown,初始為false,HelloThread在shutdown不為true的情況下一直死循環(huán),當(dāng)shutdown為true時退出并輸出"exit hello",main線程啟動HelloThread后睡了一會,然后設(shè)置shutdown為true,最后輸出"exit main"。

期望的結(jié)果是兩個線程都退出,但實際執(zhí)行,很可能會發(fā)現(xiàn)HelloThread永遠(yuǎn)都不會退出,也就是說,在HelloThread執(zhí)行流看來,shutdown永遠(yuǎn)為false,即使main線程已經(jīng)更改為了true。

這是怎么回事呢?這就是內(nèi)存可見性問題。在計算機(jī)系統(tǒng)中,除了內(nèi)存,數(shù)據(jù)還會被緩存在CPU的寄存器以及各級緩存中,當(dāng)訪問一個變量時,可能直接從寄存器或CPU緩存中獲取,而不一定到內(nèi)存中去取,當(dāng)修改一個變量時,也可能是先寫到緩存中,而稍后才會同步更新到內(nèi)存中。在單線程的程序中,這一般不是個問題,但在多線程的程序中,尤其是在有多CPU的情況下,這就是個嚴(yán)重的問題。一個線程對內(nèi)存的修改,另一個線程看不到,一是修改沒有及時同步到內(nèi)存,二是另一個線程根本就沒從內(nèi)存讀。

怎么解決這個問題呢?有多種方法:

  • 使用volatile關(guān)鍵字
  • 使用synchronized關(guān)鍵字或顯式鎖同步

關(guān)于這些方法,我們在后續(xù)章節(jié)再介紹。

線程的優(yōu)點及成本

優(yōu)點

為什么要創(chuàng)建單獨的執(zhí)行流?或者說線程有什么優(yōu)點呢?至少有以下幾點:

  • 充分利用多CPU的計算能力,單線程只能利用一個CPU,使用多線程可以利用多CPU的計算能力。
  • 充分利用硬件資源,CPU和硬盤、網(wǎng)絡(luò)是可以同時工作的,一個線程在等待網(wǎng)絡(luò)IO的同時,另一個線程完全可以利用CPU,對于多個獨立的網(wǎng)絡(luò)請求,完全可以使用多個線程同時請求。
  • 在用戶界面(GUI)應(yīng)用程序中,保持程序的響應(yīng)性,界面和后臺任務(wù)通常是不同的線程,否則,如果所有事情都是一個線程來執(zhí)行,當(dāng)執(zhí)行一個很慢的任務(wù)時,整個界面將停止響應(yīng),也無法取消該任務(wù)。
  • 簡化建模及IO處理,比如,在服務(wù)器應(yīng)用程序中,對每個用戶請求使用一個單獨的線程進(jìn)行處理,相比使用一個線程,處理來自各種用戶的各種請求,以及各種網(wǎng)絡(luò)和文件IO事件,建模和編寫程序要容易的多。

成本

關(guān)于線程,我們需要知道,它是有成本的。創(chuàng)建線程需要消耗操作系統(tǒng)的資源,操作系統(tǒng)會為每個線程創(chuàng)建必要的數(shù)據(jù)結(jié)構(gòu)、棧、程序計數(shù)器等,創(chuàng)建也需要一定的時間。

此外,線程調(diào)度和切換也是有成本的,當(dāng)有當(dāng)量可運行線程的時候,操作系統(tǒng)會忙于調(diào)度,為一個線程分配一段時間,執(zhí)行完后,再讓另一個線程執(zhí)行,一個線程被切換出去后,操作系統(tǒng)需要保存它的當(dāng)前上下文狀態(tài)到內(nèi)存,上下文狀態(tài)包括當(dāng)前CPU寄存器的值、程序計數(shù)器的值等,而一個線程被切換回來后,操作系統(tǒng)需要恢復(fù)它原來的上下文狀態(tài),整個過程被稱為上下文切換,這個切換不僅耗時,而且使CPU中的很多緩存失效,是有成本的。

當(dāng)然,這些成本是相對而言的,如果線程中實際執(zhí)行的事情比較多,這些成本是可以接受的,但如果只是執(zhí)行本節(jié)示例中的counter++,那相對成本就太高了。

另外,如果執(zhí)行的任務(wù)都是CPU密集型的,即主要消耗的都是CPU,那創(chuàng)建超過CPU數(shù)量的線程就是沒有必要的,并不會加快程序的執(zhí)行。

小結(jié)

本節(jié),我們介紹了Java中線程的一些基本概念,包括如何創(chuàng)建線程,線程的一些基本屬性和方法,多個線程可以共享內(nèi)存,但共享內(nèi)存也有兩個重要問題,一個是競態(tài)條件,另一個是內(nèi)存可見性,最后,我們討論了線程的一些優(yōu)點和成本。

以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時也希望多多支持服務(wù)器之家!

原文鏈接:http://www.cnblogs.com/swiftma/p/6395477.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 欧洲视频一区 | 日本中文字幕免费 | 国产97在线播放 | 国产一区二区三区四区五区密私 | 男人午夜天堂 | 欧美亚洲综合另类 | 成人网18免费网站 | 免费的一级视频 | 午夜看片网站 | av私库在线观看 | 久久综合久色欧美综合狠狠 | 久久久国产视频 | 91污在线观看 | 成人精品一区二区 | 国内精品视频一区二区三区八戒 | 五月婷婷综合激情网 | 免费视频一区 | 亚洲综合无码一区二区 | 一级黄色片欧美 | 欧美福利网址 | 国产精品视频导航 | 日韩国产精品一区二区三区 | av久草| aaa级大片 | 欧美精品一区二区视频 | 日韩欧美中字 | 综合亚洲精品 | 精品第一页 | 伊人在线| 中文字幕91在线 | 精品99久久久久久 | 校园春色av| 国内免费自拍视频 | 精品国产乱码久久久久久88av | sis001亚洲原创区 | 一区二区三区久久 | 久久99精品久久久久久久青青日本 | 青青草亚洲 | 亚洲精品一区在线观看 | 黄桃av| 久久精品国产99国产 |