多線程

概述

操作系統幾乎都支持多任務操作,例如:可以聽歌,可以看電影,可以聊天。 聽歌,看電影,聊天就是一個個的任務,每個運行中的任務都是一個進程。就聽歌而言,可以唱,可以顯示歌詞,這便是一個個的線程,一個進程中包含了多個順序執行流,每個順序執行流就是一個線程。

進程的定義

進程執行的程序(程序是靜態的,進程是動態的),或者是一個任務。 一個進程可以包含一個或多個線程。

線程的定義

線程就是程序中單獨順序的流控制。線程本身不能運行,它只能用于程序中。而多線程指的是單個程序中可以同時運行多個不同的線程執行不同的小任務。 線程是不擁有系統資源的,只能使用分配給程序的資源和環境。

進程可以看成是一個工廠,而線程可以看成是工廠中的工人,為了完成工廠的任務,每個工人都要去完成自己所負責的小任務, 從而使進程完成它的任務。 多進程允許多個任務同時運行,多線程是一個任務分成不同部分運行。

進程和線程的區別

  • 多個進程的內部數據和狀態都是完全獨立的,不會互相影響。 而多線程是共享一塊內存空間和一組系統資源,有可能互相影響。
  • 線程本身的數據通常只有寄存器數據,以及一個程序執行使用過的堆棧,所以線程的切換比進程切換的負擔要小。
  • 一個進程由多個線程組成,線程是進程的進行單元。當進程被初始化后,主線程就被創建了。一個線程必須有一個父進程。

一個程序運行后至少有一個進程,一個進程里可以包含多個線程,但至少要包含一個線程。

線程的創建和啟動

JAVA中使用Threadk類來代表線程,所有的線程對象都是Thread類或者子類的實例。每個線程的作用是完成一定的任務,實際上就是進行一段程序流,JAVA使用線程執行體來代表這段程序流。一個線程不能被重現啟動在它執行完成之后。

線程創建的方式:

  • 繼承Thread類,然后重寫run方法
  • 實現Runable接口 ,實現其run方法
  • 使用Callable和Future創建線程

將希望線程執行的代碼放到run方法中,然后使用start方法來啟動線程,start方法首先為線程的執行準備好系統資源,在去調用run方法。

下面看一下各個線程的實現代碼

通過繼承Thread類實現

public class ThreadTest {
    public static void main(String[] args) {
        Thread1 t = new Thread1() ;
        t.start();
    }
}

class Thread1 extends  Thread{
    @Override
    public void run(){
        for (int i = 0 ; i < 100 ; i++) {
            System.out.println("hello thread " + i );
        }
    }
}

通過實現Runable接口實現:

public class Thread2Test {
    public static void main(String[] args) {
        Thread2 t = new Thread2();
        Thread tt = new Thread(t) ;
        tt.start();
    }
}

class Thread2 implements  Runnable{

    @Override
    public void run() {
        for (int i = 0 ; i < 100 ; i++){
            System.out.println("hello world " + i);
        }
    }
}

//通過靜態內部類的方式創建多線程
public class StaticTest {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i <20 ; i++) {
                    System.out.println("hello  world " + i );
                }
            }
        }) ;
        t.start();
        for (int i = 0 ; i < 20  ; i++){
            System.out.println("main " + i );
        }
    }
}

通過上面兩種線程啟動方式的比較,是否會有這樣的疑問,繼承Thread為什么要重寫run方法,通過實現Runable接口的類,為什么要作為Thread類的構造參數,才能啟動線程,取源碼中尋找一下答案。

public
class Thread implements Runnable {  //Thread類也實現了Runable接口
 //無參構造方法
 public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
 //參數為Runable類型的構造方法
 public Thread(Runnable target) {
      init(null, target, "Thread-" + nextThreadNum(), 0);
 }
 
 //線程默認的名字是Thread-number ,通過下面的代碼實現, threadInitNumber靜態成員變量,被所有的線程對象共享
 private static int threadInitNumber;
 private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }
 public Thread(String name) {
        init(null, null, name, 0);  //指定線程的名字
    }
 //... 還有其他的構造方法,不再羅列

 //私有初始化方法
 private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
        init(g, target, name, stackSize, null);
    }

 public synchronized void start() {
  
        if (threadStatus != 0)
         
        group.add(this);

        boolean started = false;
        try {
            start0();   //start0 為一個native方法,調用c來分配系統資源
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
 //run方法,如果target為空,則什么都不做,否則執行target的run方法。
 public void run() {
        if (target != null) {
            target.run();
        }
    }
 //調用getName方法可以返回線程的名字
 public final String getName() {
        return new String(name, true);
    }

 //由于主要介紹多線程,Thread的其他方法不再過多的羅列,可以自行查閱源碼文件
}

當使用第一種方式來生成線程對象的時候,必須要重寫run方法,因為Thread類中的run方法,當target為空時,并不做任何的操作。第二方式來生成線程對象時,需要實現run方法,然后使用new Thread(new myThread())來生成線程對象。target不為空,就會調用target類的run 方法 。

使用Callable和Future創建線程

public class Thread3Test {
    public static void main(String[] args) {
        ThirdThread t3 = new ThirdThread() ;
        FutureTask<Integer> futureTask = new FutureTask<Integer>(t3);

        ThirdThread1 t4 = new ThirdThread1() ;
        FutureTask<Integer>  futureTask1 = new FutureTask<Integer>(t4);

        Thread t = new Thread(futureTask);
        Thread t1 = new Thread(futureTask1);
        t.start();
        t1.start();
        try {
            System.out.println(futureTask.get());
            System.out.println(futureTask1.get());
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class ThirdThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0 ;
        for ( ; i <5 ; i++){
            System.out.println("hello " + i );
        }
        return i ;
    }
}
class ThirdThread1 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0 ;
        for ( ; i <5 ; i++){
            System.out.println("world " + i );
        }
        return i ;
    }
}

//運行結果 ,多線程的運行結果是不可預測的,可能回和我的運行結果不同
hello 0
world 0
hello 1
world 1
hello 2
world 2
world 3
world 4
hello 3
hello 4
5
5
main

為了說明效果,特寫了兩個類,啟動了兩個線程,從結果看,多線程的效果確實是產生了。但是通過上面的代碼發現,使用了FutureTask對實現了Callable接口的類,進行了包裝,然后將FutureTask實例傳入了Thread類的構造方法中,這又是為啥? Thread的構造方法只能接受Runable的類型,Call接口并沒有繼承Runable接口,無法作為參數傳入Thread構造方法中,所以使用了FutureTask來進行包裝,因為FutureTask實現了Runable接口,并實現了run方法,在該方法中調用了Callable接口的call方法,并將call方法的返回值賦值給了result變量。調用get()方法便能獲取返回值。

三種創建方式總結:

通過繼承Thread類,實現Runable接口和Callable接口都可以創建多線程,繼承Thread類,并重寫run方法 ,可以直接通過調用start()方法實現多線程,實現Runable接口,則需要借助Thread類實現,將Runable實例,傳入Thread的構造方法中,在調用start()方法實現多線程。實現Callable接口,則需要使用FutureTask來包裝,并將FutureTask實例傳入Thread的構造方法中實現多線程。實現Runable和Callable接口的不同之處是,Callable接口中的call方法可以返回值,并可以拋出異常。

線程的生命周期

線程要經過:新建、就緒、運行、阻塞、死亡五種狀態。

生命周期的轉化如下:


線程的生命周期.png

線程同步

在多線程的環境中,可能會有兩個甚至多個的線程試圖同時訪問一個有限的資源。對于這種潛在的資源沖突進行預防。解決的方法便是在資源上為其加鎖,對象加鎖之后,其他線程便不能再訪問加鎖的資源。

線程安全的問題,比較突出的便是銀行賬戶的問題,好多書中都是拿這個在說明多線程對有限資源的競爭問題。

為了解決對這種有限資源的競爭,java提供了synchronized關鍵字來解決這個問題。 synchronized可以修飾方法,被synchronized修飾的方法稱為同步方法。

java中的每個對象都有一個鎖(lock)或者叫做監視器(monitor),當訪問某個對象的synchronized方法時,表示將該對象上鎖,此時其他的任何線程都無法在去訪問該synchronized方法了,直到之前的那個線程執行完畢后(或者拋出異常),將該對象的鎖釋放掉,其他線程才有可能再去訪問synchronized方法。

對于同步方法而言,無須顯式的指定同步監視器,同步方法的同步監視器是this,也就是對象本身。線程在開始進行同步方法或者同步代碼塊之前,必須先獲得對同步監視器的鎖定。所以任意時刻只能有一個線程獲得對對象的訪問操作。

synchronized方法是一種粗粒度的并發控制,synchronized塊是一種細粒度的并發控制,范圍更小一點。

Java5之后,java提供了一種功能更加強大的線程同步機制,顯示的定義同步鎖對象來實現同步,在這種機制下,同步鎖使用Lock對象充當。

代碼如下:

class LockTest{
    private final ReentrantLock lock = new ReentrantLock();
    public void method(){
        lock.lock();
        try{
            ....
        }finally{
            lock.unlock() ;
        }
    }
}

使用Lock與使用同步方法有點相似,使用Lock時時顯式使用Lock對象作為同步鎖,同步方法是隱式使用當前對象作為同步監視器。

死鎖

當兩個線程相互等待對象釋放同步監視器時就會發生死鎖,一旦發生死鎖,程序不會出現任何異常和任何提示,所以應該避免線程之間相互等待對方釋放資源的情況出現。

傳統的線程通信

傳統的線程通信,借助了Object類中的wait() ,notify(),notifyAll()三個方法,這三個方法并不屬于Thread類。

如果使用sychronized來保證同步,通信則依靠下面的方法:

wait():導致當前線程阻塞,知道其他線程調用該同步監視器的notify()或者notifyAll()方法,該線程才會被喚醒,喚醒后并不會立即執行,而是等待處理器的調度。
notify():喚醒此同步監視器中等待的單個線程。
notifyAll():喚醒同步監視器中等待的所有線程。

wait()和notify() 或 wait()和notifyAll()是成對出現的,因為他們依靠的都是同一個同步監視器。

使用Condition控制線程通信

如果程序沒有使用synchronized來保證同步,而是使用了Lock對象來保證同步,則系統中就不存在隱式的同步監視器,也就不能使用wait(),notify(),notifyAll()方法進行通信了。 Java提供了Condition來進行線程之間的通信。

Condition提供了3個方法來進行線程通信:
await(): 導致當前線程等待,知道其他線程調用signal()或者signalAll()方法
signal(): 喚醒此Lock對象上的單個線程。
signalAll():喚醒此Lock對象上的所有線程。

線程池

創建一個新的線程成本還是比較高的,因為它涉及了程序與操作系統之間的交互,所以使用線程池可以很好的提高性能,尤其是程序的線程的生命周期很短,需要頻繁的創建線程的時候。

Java5提供了內建的線程池支持,新增了一個Executors工廠類來產生線程。

  • newCachedThreadPool():創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中 。
  • newFixedThreadPool(int nThreads): 創建一個可重用,固定線程數的線程池。
  • newSingleThreadPool():創建一個只有單線程的線程池

上面三個方法返回ExecutorService對象代表一個線程池,可以執行Runnable對象或者Callable對象所代表的線程

下面這兩個方法返回一個ScheduledExecutorService線程池,是ExecutorService的子類,可以在指定的延遲后執行任務。

  • newScheduledThreadPool(int corePoolSize): 創建具有指定線程數的線程池,它可以在指定延遲后執行線程任務,corePoolSize指定池中保存的線程數,即使線程是空閑的也被保存在線程池中。
  • newSingleThreadScheduledExecutor():創建只有一個線程的線程池,它可以在指定延遲后執行線程任務。

線程池的簡單使用:

public class MyThread extends Thread{
    public void run(){
        for(itn i = 0 ; i <10 ; i++){
            System.out.println("hello world" + i) ;
        }
    }
}

public class Test{
    public static void main(String[] args){
        MyThread t = new MyThread();
        ExecutorService pool = Exectors.newFixedThreadPool(6);
        pool.submit(t) ;
        pool.shutdown();
    }
}


少年聽雨歌樓上,紅燭昏羅帳。  
壯年聽雨客舟中,江闊云低,斷雁叫西風。
感謝支持!
                                        ---起個名忒難

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 該文章轉自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍閱讀 7,381評論 3 87
  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,792評論 12 45
  • 標簽(空格分隔): java java 5新增機制--->同步鎖(Lock) 從java5開始,java提供了一種...
    Sivin閱讀 563評論 1 5
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,994評論 1 18
  • 修行能達到終極‘宇宙神’的極少極少,任何一座圣界內的宇宙神都屈指可數,像母祖界更是僅僅只有母祖一位。而銀發金角人正...
    im喵小姐閱讀 302評論 0 0