Java編程-多線程基礎(chǔ)

什么是線程

從概念上來說,線程不難理解合是。指的是程序代碼的獨(dú)立執(zhí)行路徑(it's an independent path of execution through program code)存崖。當(dāng)多線程執(zhí)行的時候,一個線程執(zhí)行的代碼通常與另一個線程執(zhí)行的代碼不同。那么JVM是怎么管理每個線程的執(zhí)行呢蜗帜?JVM給每一個線程一個方法調(diào)用棧(method-call stack),除了跟蹤當(dāng)前的二進(jìn)制命令之外资厉,還跟蹤局部變量和JVM傳過來的參數(shù)厅缺,以及方法的返回值。
Java通過java.lang.Thread來完成多線程功能宴偿,每個Thread對象都對應(yīng)一個線程執(zhí)行體湘捎。線程執(zhí)行的內(nèi)容在Thread的run方法中,由于默認(rèn)run方法是一個空方法窄刘,我們可以通過繼承Thread類窥妇,重寫run方法來實(shí)現(xiàn)我們的工作。

Program 1: ThreadDemo.java

//ThreadDemo.java
class ThreadDemo
{
    public static void main(String[] args)
    {
        MyThread mt = new MyThread();
        mt.start();
        for (int i = 0; i < 50; i++)
        {
            System.out.println("i = " + i + ", i * i = " + i * i);
        }
    }

}

class MyThread extends Thread
{
    @Override
    public void run()
    {
        for (int count = 1, row = 1; row < 20; count++, row++)
        {
            for (int i = 0; i < count; i++)
            {
                System.out.print('*');
            }
            System.out.println();
        }
    }
}

當(dāng)我們使用java ThreadDemo去執(zhí)行上述代碼時娩践,JVM創(chuàng)建了一個主線程執(zhí)行main方法活翩,通過執(zhí)行mt.start()之后烹骨,主線程通知JVM去創(chuàng)建另一個線程用來執(zhí)行MyThread當(dāng)中的run方法。當(dāng)start()方法返回之后材泄,主線程繼續(xù)執(zhí)行for的代碼塊沮焕,而另一個線程執(zhí)行run方法。

Thread類

為了能夠更熟練地使用Java多線程拉宗。我們需要了解構(gòu)成Thread類的一些方法峦树,他們包括如何開始一個線程,如何為線程命名旦事,如何讓線程暫停魁巩,如何確定一個線程是否活躍,如果將一個線程連接至另一個線程族檬,如何得到當(dāng)前上下文活躍線程數(shù)量歪赢,以及如何做多線程的調(diào)試。

創(chuàng)建一個線程

線程有八個構(gòu)造器单料,最簡單的是

Thread() //創(chuàng)建一個具有默認(rèn)名字的線程實(shí)例
Thread(String name) //創(chuàng)建一個具有指定名字的線程實(shí)例

與之對應(yīng)的還有

Thread(Runnable target)
Thread(Runnable target, String name)

上面兩組構(gòu)造器埋凯,不同的只有Runnable參數(shù),Runnable參數(shù)確定了那些Thread類之外的提供run函數(shù)的對象扫尖。最后四個構(gòu)造器組合了上述構(gòu)造器白对,同時添加了ThreadGroup參數(shù)。
其中構(gòu)造器

Thread(ThreadGroup group, Runnable target, String name, long stackSize)

可以讓你指定方法調(diào)用棧(method-call stack)的深度换怖,這對于一些以遞歸方式實(shí)現(xiàn)的方法來說很有用(可以避免StackOverFlowErrors)甩恼。
Thread類和Thread類的子類,它們都不是具體的線程沉颂。它們描述了線程的屬性条摸,例如線程的名字和線程執(zhí)行的run方法。在調(diào)用一個Thread類的start方法時铸屉,JVM會按照Thread類描述的模板去創(chuàng)建一個線程并調(diào)用run方法钉蒲。
在調(diào)試階段,區(qū)分一個線程和其他的線程是很有必要的彻坛。Java將線程的名字和線程相綁定顷啼。線程的名字默認(rèn)為"Thread"+"-"+"從0開始的整型",例如"Thread-0"昌屉。我們在構(gòu)造器里面可以傳入自定義的字符串作為名字钙蒙。

線程的Sleep

調(diào)用Thread的靜態(tài)方法sleep(long millis)可以強(qiáng)制一個線程暫停millis毫秒。其他線程可以中斷正在休眠的線程间驮,如果發(fā)生了躬厌,那么正在休眠的線程清醒然后拋出一個InterruptedException。因此sleep方法必須包含在try塊或者調(diào)用sleep的方法拋出一個異常竞帽。
下面使用一個計算pi(圓周率)的程序來說明sleep的作用烤咧。

Program 2: CalcPI1.java

//CalcPI1.java
class CalcPI1
{
   public static void main (String [] args)
   {
      MyThread mt = new MyThread ();
      mt.start ();
      try
      {
          Thread.sleep (10); // Sleep for 10 milliseconds
      }
      catch (InterruptedException e)
      {
      }
      System.out.println ("pi = " + mt.pi);
   }
}
class MyThread extends Thread
{
   boolean negative = true;
   double pi; // Initializes to 0.0, by default
   public void run ()
   {
      for (int i = 3; i < 100000; i += 2)
      {
           if (negative)
               pi -= (1.0 / i);
           else
               pi += (1.0 / i);
           negative = !negative;
      }
      pi += 1.0;
      pi *= 4.0;
      System.out.println ("Finished calculating PI");
   }
}

可以注釋掉try...catch語句塊來看看不調(diào)用sleep和調(diào)用sleep的差別偏陪。可以看出如果注釋掉try...catch塊后煮嫌,PI的值打印出來為0,那是因?yàn)橹骶€程執(zhí)行的比計算PI的線程快抱虐,所以在PI計算完成之前已經(jīng)將PI的值打印了出來昌阿。因此為了得到正確的PI值,我們要讓主線程休眠一會等待另一個線程完成PI的計算恳邀。

線程的死活

當(dāng)程序調(diào)用了線程實(shí)例的start方法并在調(diào)用run方法前會有一段時間(用于線程的初始化)懦冰。當(dāng)線程的run方法返回后并在JVM清理這個線程之前也有一段時間。在run方法調(diào)用前的一瞬間到run方法返回后的一瞬間之間的時間谣沸,Thread的isAlive方法會返回true刷钢,其余時間返回false。



當(dāng)一個線程的運(yùn)行依賴于另外一個線程的結(jié)果時乳附,isAlive方法就變得很有用了内地。通過while循環(huán)不斷調(diào)用isAlive方法,直到返回false赋除。這樣就可以確保一個線程在另一個線程結(jié)束后運(yùn)行阱缓。
下面給出一個修改版本的PI計算代碼

Program 3: CalcPI2.java

//CalcPI2.java
class CalcPI2
{
   public static void main (String [] args)
   {
      MyThread mt = new MyThread ();
      mt.start ();
      while (mt.isAlive ())
        try
        {
            Thread.sleep (10); // Sleep for 10 milliseconds
        }
        catch (InterruptedException e)
        {
        }
      System.out.println ("pi = " + mt.pi);
   }
}
class MyThread extends Thread
{
   boolean negative = true;
   double pi; // Initializes to 0.0, by default
   public void run ()
   {
      for (int i = 3; i < 100000; i += 2)
      {
           if (negative)
               pi -= (1.0 / i);
           else
               pi += (1.0 / i);
           negative = !negative;
      }
      pi += 1.0;
      pi *= 4.0;
      System.out.println ("Finished calculating PI");
   }
}

線程的Join

由于線程的sleep方法和isAlive方法都很實(shí)用,所以Sun將它們打包成了三個方法:join()举农、join(long millis)join(long millis, int nanos)荆针。當(dāng)一個線程想要等待另一個線程結(jié)束時,這個線程可以通過另一個線程的引用來調(diào)用join方法颁糟。下面為PI計算代碼的Join方法實(shí)現(xiàn)航背。注意默認(rèn)不帶參數(shù)的join方法是阻塞的,直到線程結(jié)束為止棱貌,而使用join(long millis)可以設(shè)置一個超時時間millis玖媚。

Program 4: CalcPI3.java

// CalcPI3.java
class CalcPI3
{
   public static void main (String [] args)
   {
      MyThread mt = new MyThread ();
      mt.start ();
      try
      {
          mt.join ();
      }
      catch (InterruptedException e)
      {
      }
      System.out.println ("pi = " + mt.pi);
   }
}
class MyThread extends Thread
{
   boolean negative = true;
   double pi; // Initializes to 0.0, by default
   public void run ()
   {
      for (int i = 3; i < 100000; i += 2)
      {
           if (negative)
               pi -= (1.0 / i);
           else
               pi += (1.0 / i);
           negative = !negative;
      }
      pi += 1.0;
      pi *= 4.0;
      System.out.println ("Finished calculating PI");
   }
}

統(tǒng)計線程的情況

在某些場景下,我們可能希望知道當(dāng)前程序里面有多少個活躍的線程键畴。Thread類提供了一對方法用來幫助你完成這些工作:activeCount()enumerate(Thread[] threads)最盅。但是這些方法只能在當(dāng)前線程所屬線程組的上下文里工作。換句話說起惕,這些方法只能找到與當(dāng)前線程屬于同一個線程組的活躍線程涡贱。
靜態(tài)方法activeCount()返回當(dāng)前線程組活躍的線程數(shù)量。一個程序使用activeCount()的整型返回值線程引用數(shù)組的大小惹想。為了獲得這些引用问词,程序必須調(diào)用靜態(tài)的enumerate(Thread[] threads)方法,該方法的整型返回值線程引用數(shù)組的數(shù)量嘀粱。

Program 5: ThreadCensus.java

//ThreadCensus.java
public class ThreadCensus
{
   public static void main(String[] args)
   {
       Thread[] threads = new Thread[Thread.activeCount()];
       int n = Thread.enumerate(threads);
       for (int i = 0; i < n; i++)
       {
           System.out.println(threads[i]);
       }
   }
}

上述例子說明了這對方法的使用激挪。輸出結(jié)果應(yīng)該為Thread[main, 5, main]辰狡。第一個main代表線程的名字,5代表線程的優(yōu)先級垄分,第二個main代表該線程屬于哪個線程組宛篇。我們可能會覺得奇怪,為什么在輸出沒有看到系統(tǒng)的線程薄湿。Thread的靜態(tài)方法enumerate(Thread[] threads)只能詢問與當(dāng)前線程屬于同一個線程組的活躍線程叫倍。但是在ThreadGroup類里面包含了多個enumerate()方法允許你去捕捉所有線程的引用。ThreadGroup將在之后的章節(jié)中提及豺瘤。
不要依賴activeCount返回的結(jié)果:因?yàn)樵赼ctiveCount方法到enumerate方法之間吆倦,有可能有線程會終止,這樣會導(dǎo)致activeCount返回值不在有效坐求,用這個返回值去遍歷線程引用數(shù)組時會發(fā)生越界的錯誤蚕泽。

線程調(diào)試

如果你的程序遇到故障,而你發(fā)現(xiàn)這個故障可能與一個線程有關(guān)桥嗤,你可以通過Thread.dumpStack()來取得這個線程的詳細(xì)信息须妻。靜態(tài)的dumpStack()方法提供了new Exception("Stack trace").printStackTrace()的包裹。

線程的等級

并不是所有線程都是平等的砸逊。線程分為兩類璧南,一種是用戶線程,一種是守護(hù)線程师逸。一個用戶線程執(zhí)行用戶程序的工作司倚,這些工作必須在應(yīng)用終止之前完成。而守護(hù)線程一般執(zhí)行內(nèi)務(wù)(例如垃圾回收器(garbage collection))和其他的后臺任務(wù)篓像,這些后臺任務(wù)不需要依賴于應(yīng)用的主要工作动知,但是應(yīng)用的主要工作需要這些后臺任務(wù)。與用戶線程不同的是员辩,守護(hù)線程不需要在用戶線程結(jié)束之前完成任務(wù)盒粮。當(dāng)一個應(yīng)用的開始線程(即用戶線程)終止時,JVM會檢查其他用戶線程是否還在運(yùn)行奠滑,如果一些還在丹皱,那么JVM會阻止該應(yīng)用的終止。但如果是守護(hù)線程的話宋税,JVM會不管是否有后臺線程還在運(yùn)行終止這個應(yīng)用摊崭。如果想要得到當(dāng)前線程的引用,可以使用Thread.currentThread()獲得杰赛。
當(dāng)你調(diào)用線程對象的start()方法時呢簸,新創(chuàng)建的線程是用戶線程(默認(rèn))。如果想要創(chuàng)建一個守護(hù)線程,在調(diào)用start()方法前根时,需要調(diào)用setDeamon()方法來設(shè)置瘦赫。我們可以使用Thread.isDaemon()來判斷一個線程是否為守護(hù)線程。

Program 6: UserDaemonThreadDemo.java

//UserDaemonThreadDemo.java
public class UserDaemonThreadDemo
{
    public static void main(String[] args)
    {
        if (args.length == 0)
        {
            new MyThread().start();
        }
        else
        {
            MyThread mt = new MyThread();
            mt.setDaemon(true);
            mt.start();
        }
        try
        {
            Thread.sleep(100);
        }
        catch(InterruptedException e)
        {

        }
    }
}

class MyThread extends Thread
{
    @Override
    public void run()
    {
        System.out.println("Daemon:" + isDaemon());
        while(true);
    }
}

上述程序根據(jù)是否傳入命令行參數(shù)來創(chuàng)建守護(hù)線程或用戶線程蛤迎。如果是用戶線程确虱,程序會一致運(yùn)行,我們需要按下Ctrl+C來終止程序忘苛,如果是守護(hù)線程蝉娜,守護(hù)進(jìn)程會隨著主線程的終止而終止。

使用Runnable創(chuàng)建線程

除了通過擴(kuò)展Thread類扎唾,重寫run方法來創(chuàng)建之外,還有別的方式創(chuàng)建線程南缓。我們知道Java中不允許多繼承的存在胸遇,一個類如果繼承自非線程類,那么它就不能繼承自線程類汉形。由于繼承的限制纸镊,我們?nèi)绾伟讯嗑€程引入到一個其他類的子類呢,Java提供了使用Runnable來創(chuàng)建線程的方法概疆。使用Runnable實(shí)現(xiàn)計算PI的線程逗威。

Program 7: CalcPI4.java

//CalcPI4.java
class CalcPI4
{
   public static void main (String [] args)
   {
      MyThread runnable = new MyThread();
      Thread mt = new Thread(runnable);
      mt.start ();
      try
      {
          Thread.sleep (10); // Sleep for 10 milliseconds
      }
      catch (InterruptedException e)
      {
      }
      System.out.println ("pi = " + runnable.pi);
   }
}
class MyThread implements Runnable
{
   boolean negative = true;
   double pi; // Initializes to 0.0, by default
   public void run ()
   {
      for (int i = 3; i < 100000; i += 2)
      {
           if (negative)
               pi -= (1.0 / i);
           else
               pi += (1.0 / i);
           negative = !negative;
      }
      pi += 1.0;
      pi *= 4.0;
      System.out.println ("Finished calculating PI");
   }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市岔冀,隨后出現(xiàn)的幾起案子凯旭,更是在濱河造成了極大的恐慌,老刑警劉巖使套,帶你破解...
    沈念sama閱讀 212,657評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罐呼,死亡現(xiàn)場離奇詭異,居然都是意外死亡侦高,警方通過查閱死者的電腦和手機(jī)嫉柴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,662評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奉呛,“玉大人计螺,你說我怎么就攤上這事∏谱常” “怎么了登馒?”我有些...
    開封第一講書人閱讀 158,143評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長馁痴。 經(jīng)常有香客問我谊娇,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,732評論 1 284
  • 正文 為了忘掉前任济欢,我火速辦了婚禮赠堵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘法褥。我一直安慰自己茫叭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,837評論 6 386
  • 文/花漫 我一把揭開白布半等。 她就那樣靜靜地躺著揍愁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杀饵。 梳的紋絲不亂的頭發(fā)上莽囤,一...
    開封第一講書人閱讀 50,036評論 1 291
  • 那天,我揣著相機(jī)與錄音切距,去河邊找鬼朽缎。 笑死,一個胖子當(dāng)著我的面吹牛谜悟,可吹牛的內(nèi)容都是我干的话肖。 我是一名探鬼主播,決...
    沈念sama閱讀 39,126評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼葡幸,長吁一口氣:“原來是場噩夢啊……” “哼最筒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蔚叨,我...
    開封第一講書人閱讀 37,868評論 0 268
  • 序言:老撾萬榮一對情侶失蹤床蜘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后缅叠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悄泥,經(jīng)...
    沈念sama閱讀 44,315評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,641評論 2 327
  • 正文 我和宋清朗相戀三年肤粱,在試婚紗的時候發(fā)現(xiàn)自己被綠了弹囚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,773評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡领曼,死狀恐怖鸥鹉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庶骄,我是刑警寧澤毁渗,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站单刁,受9級特大地震影響灸异,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一肺樟、第九天 我趴在偏房一處隱蔽的房頂上張望檐春。 院中可真熱鬧,春花似錦么伯、人聲如沸疟暖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,859評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俐巴。三九已至,卻和暖如春硬爆,著一層夾襖步出監(jiān)牢的瞬間欣舵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工缀磕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留邻遏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,584評論 2 362
  • 正文 我出身青樓虐骑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赎线。 傳聞我的和親對象是個殘疾皇子廷没,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,676評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 本文主要講了java中多線程的使用方法、線程同步垂寥、線程數(shù)據(jù)傳遞颠黎、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等滞项。 首先講...
    李欣陽閱讀 2,444評論 1 15
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的狭归,為什么轉(zhuǎn)載兩個字加“”呢?因?yàn)檫@絕不是簡單的復(fù)制粘貼,我花了五六個小...
    SmartSean閱讀 4,717評論 12 45
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,952評論 1 18
  • 線程概述 線程與進(jìn)程 進(jìn)程 ?每個運(yùn)行中的任務(wù)(通常是程序)就是一個進(jìn)程。當(dāng)一個程序進(jìn)入內(nèi)存運(yùn)行時臀蛛,即變成了一個進(jìn)...
    閩越布衣閱讀 1,008評論 1 7
  • 木匠皇帝和他的海鮮火鍋 臺北故宮博物院藏《天啟皇帝朝服像》中选泻,他的御用家具及家具上的陳設(shè)要比明朝其他皇帝量多而精美...
    皮皮的爸爸閱讀 1,671評論 0 4