編程思想之多線程與多進程(3)——Java中的多線程

原文鏈接:https://blog.csdn.net/luoweifu/article/details/46673975

原文:http://blog.csdn.net/luoweifu/article/details/46673975
作者:luoweifu
轉載請標名出處


編程思想之多線程與多進程(1)——以操作系統的角度述說線程與進程》一文詳細講述了線程、進程的關係及在操作系統中的表現,這是多線程學習必須瞭解的基礎。本文將接着講一下Java中多線程程序的開發

單線程

任何程序至少有一個線程,即使你沒有主動地創建線程,程序從一開始執行就有一個默認的線程,被稱爲主線程,只有一個線程的程序稱爲單線程程序。如下面這一簡單的代碼,沒有顯示地創建一個線程,程序從main開始執行,main本身就是一個線程(主線程),單個線程從頭執行到尾。

【Demo1】:單線程程序

public static void main(String args[]) {
   System.out.println("輸出從1到100的數:");
   for (int i = 0; i < 100; i ++) {
      System.out.println(i + 1);
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

創建線程

單線程程序簡單明瞭,但有時無法滿足特定的需求。如一個文字處理的程序,我在打印文章的同時也要能對文字進行編輯,如果是單線程的程序則要等打印機打印完成之後你才能對文字進行編輯,但打印的過程一般比較漫長,這是我們無法容忍的。如果採用多線程,打印的時候可以單獨開一個線程去打印,主線程可以繼續進行文字編輯。在程序需要同時執行多個任務時,可以採用多線程。

在程序需要同時執行多個任務時,可以採用多線程。Java給多線程編程提供了內置的支持,提供了兩種創建線程方法:1.通過實現Runable接口;2.通過繼承Thread類。

Thread是JDK實現的對線程支持的類,Thread類本身實現了Runnable接口,所以Runnable是顯示創建線程必須實現的接口; Runnable只有一個run方法,所以不管通過哪種方式創建線程,都必須實現run方法。我們可以看一個例子。

【Demo2】:線程的創建和使用

/**
 * Created with IntelliJ IDEA.
 * User: luoweifu
 * Date: 15-5-24
 * Time: 下午9:30
 * To change this template use File | Settings | File Templates.
 */

/**
 * 通過實現Runnable方法
 */
class ThreadA implements Runnable {
   private Thread thread;
   private String threadName;
   public ThreadA(String threadName) {
      thread = new Thread(this, threadName);
      this.threadName = threadName;
   }

   //實現run方法
   public void run() {
      for (int i = 0; i < 100; i ++) {
         System.out.println(threadName + ": " + i);
      }
   }

   public void start() {
      thread.start();
   }
}

/**
 * 繼承Thread的方法
 */
class ThreadB extends Thread {
   private String threadName;

   public ThreadB(String threadName) {
      super(threadName);
      this.threadName = threadName;
   }

   //實現run方法
   public void run() {
      for (int i = 0; i < 100; i ++) {
         System.out.println(threadName + ": " + i);
      }
   }
}

public class MultiThread{

   public static void main(String args[]) {
      ThreadA threadA = new ThreadA("ThreadA");
      ThreadB threadB = new ThreadB("ThreadB");
      threadA.start();
      threadB.start();
   }
}
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

說明:上面的例子中例舉了兩種實現線程的方式。大部分情況下選擇實現Runnable接口的方式會優於繼承Thread的方式,因爲:
1. 從 Thread 類繼承會強加類層次;
2. 有些類不能繼承Thread類,如要作爲線程運行的類已經是某一個類的子類了,但Java只支持單繼承,所以不能再繼承Thread類了。


線程同步

線程與線程之間的關係,有幾種:

模型一:簡單的線程,多個線程同時執行,但各個線程處理的任務毫不相干,沒有數據和資源的共享,不會出現爭搶資源的情況。這種情況下不管有多少個線程同時執行都是安全的,其執行模型如下:

處理相互獨立的任務
圖 1:處理相互獨立的任務

模型二:複雜的線程,多個線程共享相同的數據或資源,就會出現多個線程爭搶一個資源的情況。這時就容易造成數據的非預期(錯誤)處理,是線程不安全的,其模型如下:

多個線程共享相同的數據或資源
圖 2:多個線程共享相同的數據或資源

在出現模型二的情況時就要考慮線程的同步,確保線程的安全。Java中對線程同步的支持,最常見的方式是添加synchronized同步鎖。

我們通過一個例子來看一下線程同步的應用。

買火車票是大家春節回家最爲關注的事情,我們就簡單模擬一下火車票的售票系統(爲使程序簡單,我們就抽出最簡單的模型進行模擬):有500張從北京到贛州的火車票,在8個窗口同時出售,保證系統的穩定性和數據的原子性。

模擬火車票售票系統
圖 3:模擬火車票售票系統

【Demo3】:火車票售票系統模擬程序

/**
 * 模擬服務器的類
 */
class Service {
   private String ticketName;    //票名
   private int totalCount;        //總票數
   private int remaining;        //剩餘票數

   public Service(String ticketName, int totalCount) {
      this.ticketName = ticketName;
      this.totalCount = totalCount;
      this.remaining = totalCount;
   }

   public synchronized int saleTicket(int ticketNum) {
      if (remaining > 0) {
         remaining -= ticketNum;
         try {        //暫停0.1秒,模擬真實系統中複雜計算所用的時間
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }

         if (remaining >= 0) {
            return remaining;
         } else {
            remaining += ticketNum;
            return -1;
         }
      }
      return -1;
   }

   public synchronized int getRemaining() {
      return remaining;
   }

   public String getTicketName() {
      return this.ticketName;
   }

}

/**
 * 售票程序
 */
class TicketSaler implements Runnable {
   private String name;
   private Service service;

   public TicketSaler(String windowName, Service service) {
      this.name = windowName;
      this.service = service;
   }

   @Override
   public void run() {
      while (service.getRemaining() > 0) {
         synchronized (this)
         {
            System.out.print(Thread.currentThread().getName() + "出售第" + service.getRemaining() + "張票,");
            int remaining = service.saleTicket(1);
            if (remaining >= 0) {
               System.out.println("出票成功!剩餘" + remaining + "張票.");
            } else {
               System.out.println("出票失敗!該票已售完。");
            }
         }
      }
   }
}
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

測試程序:

/**
 * 測試類
 */
public class TicketingSystem {
   public static void main(String args[]) {
      Service service = new Service("北京-->贛州", 500);
      TicketSaler ticketSaler = new TicketSaler("售票程序", service);
      //創建8個線程,以模擬8個窗口
      Thread threads[] = new Thread[8];
      for (int i = 0; i < threads.length; i++) {
         threads[i] = new Thread(ticketSaler, "窗口" + (i + 1));
         System.out.println("窗口" + (i + 1) + "開始出售 " + service.getTicketName() + " 的票...");
         threads[i].start();
      }

   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

結果如下:

窗口1開始出售 北京–>贛州 的票…
窗口2開始出售 北京–>贛州 的票…
窗口3開始出售 北京–>贛州 的票…
窗口4開始出售 北京–>贛州 的票…
窗口5開始出售 北京–>贛州 的票…
窗口6開始出售 北京–>贛州 的票…
窗口7開始出售 北京–>贛州 的票…
窗口8開始出售 北京–>贛州 的票…
窗口1出售第500張票,出票成功!剩餘499張票.
窗口1出售第499張票,出票成功!剩餘498張票.
窗口6出售第498張票,出票成功!剩餘497張票.
窗口6出售第497張票,出票成功!剩餘496張票.
窗口1出售第496張票,出票成功!剩餘495張票.
窗口1出售第495張票,出票成功!剩餘494張票.
窗口1出售第494張票,出票成功!剩餘493張票.
窗口2出售第493張票,出票成功!剩餘492張票.
窗口2出售第492張票,出票成功!剩餘491張票.
窗口2出售第491張票,出票成功!剩餘490張票.
窗口2出售第490張票,出票成功!剩餘489張票.
窗口2出售第489張票,出票成功!剩餘488張票.
窗口2出售第488張票,出票成功!剩餘487張票.
窗口6出售第487張票,出票成功!剩餘486張票.
窗口6出售第486張票,出票成功!剩餘485張票.
窗口3出售第485張票,出票成功!剩餘484張票.
……

在上面的例子中,涉及到數據的更改的Service類saleTicket方法和TicketSaler類run方法都用了synchronized同步鎖進行同步處理,以保證數據的準確性和原子性。

關於synchronized更詳細的用法請參見:《Java中Synchronized的用法


線程控制

在多線程程序中,除了最重要的線程同步外,還有其它的線程控制,如線程的中斷、合併、優先級等。

線程等待(wait、notify、notifyAll)

Wait:使當前的線程處於等待狀態;
Notify:喚醒其中一個等待線程;
notifyAll:喚醒所有等待線程。

詳細用法參見:《 Java多線程中wait, notify and notifyAll的使用


線程中斷(interrupt)

在Java提供的線程支持類Thread中,有三個用於線程中斷的方法:
public void interrupt(); 中斷線程。
public static boolean interrupted(); 是一個靜態方法,用於測試當前線程是否已經中斷,並將線程的中斷狀態 清除。所以如果線程已經中斷,調用兩次interrupted,第二次時會返回false,因爲第一次返回true後會清除中斷狀態。
public boolean isInterrupted(); 測試線程是否已經中斷。

【Demo4】:線程中斷的應用

/**
 * 打印線程
 */
class Printer implements Runnable {
   public void run() {
      while (!Thread.currentThread().isInterrupted()) {     //如果當前線程未被中斷,則執行打印工作
         System.out.println(Thread.currentThread().getName() + "打印中… …");
      }
      if (Thread.currentThread().isInterrupted()) {
         System.out.println("interrupted:" +  Thread.interrupted());       //返回當前線程的狀態,並清除狀態
         System.out.println("isInterrupted:" +  Thread.currentThread().isInterrupted());
      }
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

調用代碼:

Printer printer = new Printer();
Thread printerThread = new Thread(printer, "打印線程");
printerThread.start();
try {
   Thread.sleep(100);
} catch (InterruptedException e) {
   e.printStackTrace();
}
System.out.println("有緊急任務出現,需中斷打印線程.");
System.out.println("中斷前的狀態:" + printerThread.isInterrupted());
printerThread.interrupt();       // 中斷打印線程
System.out.println("中斷前的狀態:" + printerThread.isInterrupted());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

結果:

打印線程打印中… …
… …
打印線程打印中… …
有緊急任務出現,需中斷打印線程.
打印線程打印中… …
中斷前的狀態:false
打印線程打印中… …
中斷前的狀態:true
interrupted:true
isInterrupted:false

線程合併(join)

所謂合併,就是等待其它線程執行完,再執行當前線程,執行起來的效果就好像把其它線程合併到當前線程執行一樣。其執行關係如下:

線程合併的過程
圖 4:線程合併的過程

public final void join()
等待該線程終止

public final void join(long millis);
等待該線程終止的時間最長爲 millis 毫秒。超時爲 0 意味着要一直等下去。

public final void join(long millis, int nanos)
等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒

這個常見的一個應用就是安裝程序,很多大的軟件都會包含多個插件,如果選擇完整安裝,則要等所有的插件都安裝完成才能結束,且插件與插件之間還可能會有依賴關係。

【Demo5】:線程合併

/**
 * 插件1
 */
class Plugin1 implements Runnable {

   @Override
   public void run() {
      System.out.println("插件1開始安裝.");
      System.out.println("安裝中...");
      try {
         Thread.sleep(1000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      System.out.println("插件1完成安裝.");
   }
}

/**
 * 插件2
 */
class Plugin2 implements Runnable {

   @Override
   public void run() {
      System.out.println("插件2開始安裝.");
      System.out.println("安裝中...");
      try {
         Thread.sleep(2000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      System.out.println("插件2完成安裝.");
   }
}
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

合併線程的調用:

System.out.println("主線程開啓...");
Thread thread1 = new Thread(new Plugin1());
Thread thread2 = new Thread(new Plugin2());
try {
   thread1.start();   //開始插件1的安裝
   thread1.join();       //等插件1的安裝線程結束
   thread2.start();   //再開始插件2的安裝
   thread2.join();       //等插件2的安裝線程結束,才能回到主線程
} catch (InterruptedException e) {
   e.printStackTrace();
}
System.out.println("主線程結束,程序安裝完成!");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

結果如下:

主線程開啓…
插件1開始安裝.
安裝中…
插件1完成安裝.
插件2開始安裝.
安裝中…
插件2完成安裝.
主線程結束,程序安裝完成!

優先級(Priority)

線程優先級是指獲得CPU資源的優先程序。優先級高的容易獲得CPU資源,優先級底的較難獲得CPU資源,表現出來的情況就是優先級越高執行的時間越多。

Java中通過getPriority和setPriority方法獲取和設置線程的優先級。Thread類提供了三個表示優先級的常量:MIN_PRIORITY優先級最低,爲1;NORM_PRIORITY是正常的優先級;爲5,MAX_PRIORITY優先級最高,爲10。我們創建線程對象後,如果不顯示的設置優先級的話,默認爲5。

【Demo】:線程優先級

/**
 * 優先級
 */
class PriorityThread implements Runnable{
   @Override
   public void run() {
      for (int i = 0; i < 1000; i ++) {
         System.out.println(Thread.currentThread().getName() + ": " + i);
      }
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

調用代碼:

//創建三個線程
Thread thread1 = new Thread(new PriorityThread(), "Thread1");
Thread thread2 = new Thread(new PriorityThread(), "Thread2");
Thread thread3 = new Thread(new PriorityThread(), "Thread3");
//設置優先級
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(8);
//開始執行線程
thread3.start();
thread2.start();
thread1.start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

從結果中我們可以看到線程thread1明顯比線程thread3執行的快。



線程和進程相關文章:

編程思想之多線程與多進程(1)——以操作系統的角度述說線程與進程

編程思想之多線程與多進程(2)——線程優先級與線程安全

編程思想之多線程與多進程(3)——Java中的多線程

編程思想之多線程與多進程(4)——C++中的多線程


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章