多線程

概述

操作系統(tǒng)幾乎都支持多任務(wù)操作,例如:可以聽歌,可以看電影,可以聊天。 聽歌,看電影,聊天就是一個(gè)個(gè)的任務(wù),每個(gè)運(yùn)行中的任務(wù)都是一個(gè)進(jìn)程。就聽歌而言,可以唱,可以顯示歌詞,這便是一個(gè)個(gè)的線程,一個(gè)進(jìn)程中包含了多個(gè)順序執(zhí)行流,每個(gè)順序執(zhí)行流就是一個(gè)線程。

進(jìn)程的定義

進(jìn)程執(zhí)行的程序(程序是靜態(tài)的,進(jìn)程是動(dòng)態(tài)的),或者是一個(gè)任務(wù)。 一個(gè)進(jìn)程可以包含一個(gè)或多個(gè)線程。

線程的定義

線程就是程序中單獨(dú)順序的流控制。線程本身不能運(yùn)行,它只能用于程序中。而多線程指的是單個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程執(zhí)行不同的小任務(wù)。 線程是不擁有系統(tǒng)資源的,只能使用分配給程序的資源和環(huán)境。

進(jìn)程可以看成是一個(gè)工廠,而線程可以看成是工廠中的工人,為了完成工廠的任務(wù),每個(gè)工人都要去完成自己所負(fù)責(zé)的小任務(wù), 從而使進(jìn)程完成它的任務(wù)。 多進(jìn)程允許多個(gè)任務(wù)同時(shí)運(yùn)行,多線程是一個(gè)任務(wù)分成不同部分運(yùn)行。

進(jìn)程和線程的區(qū)別

  • 多個(gè)進(jìn)程的內(nèi)部數(shù)據(jù)和狀態(tài)都是完全獨(dú)立的,不會(huì)互相影響。 而多線程是共享一塊內(nèi)存空間和一組系統(tǒng)資源,有可能互相影響。
  • 線程本身的數(shù)據(jù)通常只有寄存器數(shù)據(jù),以及一個(gè)程序執(zhí)行使用過的堆棧,所以線程的切換比進(jìn)程切換的負(fù)擔(dān)要小。
  • 一個(gè)進(jìn)程由多個(gè)線程組成,線程是進(jìn)程的進(jìn)行單元。當(dāng)進(jìn)程被初始化后,主線程就被創(chuàng)建了。一個(gè)線程必須有一個(gè)父進(jìn)程。

一個(gè)程序運(yùn)行后至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程里可以包含多個(gè)線程,但至少要包含一個(gè)線程。

線程的創(chuàng)建和啟動(dòng)

JAVA中使用Threadk類來代表線程,所有的線程對(duì)象都是Thread類或者子類的實(shí)例。每個(gè)線程的作用是完成一定的任務(wù),實(shí)際上就是進(jìn)行一段程序流,JAVA使用線程執(zhí)行體來代表這段程序流。一個(gè)線程不能被重現(xiàn)啟動(dòng)在它執(zhí)行完成之后。

線程創(chuàng)建的方式:

  • 繼承Thread類,然后重寫run方法
  • 實(shí)現(xiàn)Runable接口 ,實(shí)現(xiàn)其run方法
  • 使用Callable和Future創(chuàng)建線程

將希望線程執(zhí)行的代碼放到run方法中,然后使用start方法來啟動(dòng)線程,start方法首先為線程的執(zhí)行準(zhǔn)備好系統(tǒng)資源,在去調(diào)用run方法。

下面看一下各個(gè)線程的實(shí)現(xiàn)代碼

通過繼承Thread類實(shí)現(xiàn)

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 );
        }
    }
}

通過實(shí)現(xiàn)Runable接口實(shí)現(xiàn):

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);
        }
    }
}

//通過靜態(tài)內(nèi)部類的方式創(chuàng)建多線程
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 );
        }
    }
}

通過上面兩種線程啟動(dòng)方式的比較,是否會(huì)有這樣的疑問,繼承Thread為什么要重寫run方法,通過實(shí)現(xiàn)Runable接口的類,為什么要作為Thread類的構(gòu)造參數(shù),才能啟動(dòng)線程,取源碼中尋找一下答案。

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

 //私有初始化方法
 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 為一個(gè)native方法,調(diào)用c來分配系統(tǒng)資源
            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為空,則什么都不做,否則執(zhí)行target的run方法。
 public void run() {
        if (target != null) {
            target.run();
        }
    }
 //調(diào)用getName方法可以返回線程的名字
 public final String getName() {
        return new String(name, true);
    }

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

當(dāng)使用第一種方式來生成線程對(duì)象的時(shí)候,必須要重寫run方法,因?yàn)門hread類中的run方法,當(dāng)target為空時(shí),并不做任何的操作。第二方式來生成線程對(duì)象時(shí),需要實(shí)現(xiàn)run方法,然后使用new Thread(new myThread())來生成線程對(duì)象。target不為空,就會(huì)調(diào)用target類的run 方法 。

使用Callable和Future創(chuàng)建線程

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 ;
    }
}

//運(yùn)行結(jié)果 ,多線程的運(yùn)行結(jié)果是不可預(yù)測(cè)的,可能回和我的運(yùn)行結(jié)果不同
hello 0
world 0
hello 1
world 1
hello 2
world 2
world 3
world 4
hello 3
hello 4
5
5
main

為了說明效果,特寫了兩個(gè)類,啟動(dòng)了兩個(gè)線程,從結(jié)果看,多線程的效果確實(shí)是產(chǎn)生了。但是通過上面的代碼發(fā)現(xiàn),使用了FutureTask對(duì)實(shí)現(xiàn)了Callable接口的類,進(jìn)行了包裝,然后將FutureTask實(shí)例傳入了Thread類的構(gòu)造方法中,這又是為啥? Thread的構(gòu)造方法只能接受Runable的類型,Call接口并沒有繼承Runable接口,無法作為參數(shù)傳入Thread構(gòu)造方法中,所以使用了FutureTask來進(jìn)行包裝,因?yàn)镕utureTask實(shí)現(xiàn)了Runable接口,并實(shí)現(xiàn)了run方法,在該方法中調(diào)用了Callable接口的call方法,并將call方法的返回值賦值給了result變量。調(diào)用get()方法便能獲取返回值。

三種創(chuàng)建方式總結(jié):

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

線程的生命周期

線程要經(jīng)過:新建、就緒、運(yùn)行、阻塞、死亡五種狀態(tài)。

生命周期的轉(zhuǎn)化如下:


線程的生命周期.png

線程同步

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

線程安全的問題,比較突出的便是銀行賬戶的問題,好多書中都是拿這個(gè)在說明多線程對(duì)有限資源的競(jìng)爭(zhēng)問題。

為了解決對(duì)這種有限資源的競(jìng)爭(zhēng),java提供了synchronized關(guān)鍵字來解決這個(gè)問題。 synchronized可以修飾方法,被synchronized修飾的方法稱為同步方法。

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

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

synchronized方法是一種粗粒度的并發(fā)控制,synchronized塊是一種細(xì)粒度的并發(fā)控制,范圍更小一點(diǎn)。

Java5之后,java提供了一種功能更加強(qiáng)大的線程同步機(jī)制,顯示的定義同步鎖對(duì)象來實(shí)現(xiàn)同步,在這種機(jī)制下,同步鎖使用Lock對(duì)象充當(dāng)。

代碼如下:

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

使用Lock與使用同步方法有點(diǎn)相似,使用Lock時(shí)時(shí)顯式使用Lock對(duì)象作為同步鎖,同步方法是隱式使用當(dāng)前對(duì)象作為同步監(jiān)視器。

死鎖

當(dāng)兩個(gè)線程相互等待對(duì)象釋放同步監(jiān)視器時(shí)就會(huì)發(fā)生死鎖,一旦發(fā)生死鎖,程序不會(huì)出現(xiàn)任何異常和任何提示,所以應(yīng)該避免線程之間相互等待對(duì)方釋放資源的情況出現(xiàn)。

傳統(tǒng)的線程通信

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

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

wait():導(dǎo)致當(dāng)前線程阻塞,知道其他線程調(diào)用該同步監(jiān)視器的notify()或者notifyAll()方法,該線程才會(huì)被喚醒,喚醒后并不會(huì)立即執(zhí)行,而是等待處理器的調(diào)度。
notify():?jiǎn)拘汛送奖O(jiān)視器中等待的單個(gè)線程。
notifyAll():喚醒同步監(jiān)視器中等待的所有線程。

wait()和notify() 或 wait()和notifyAll()是成對(duì)出現(xiàn)的,因?yàn)樗麄円揽康亩际峭粋€(gè)同步監(jiān)視器。

使用Condition控制線程通信

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

Condition提供了3個(gè)方法來進(jìn)行線程通信:
await(): 導(dǎo)致當(dāng)前線程等待,知道其他線程調(diào)用signal()或者signalAll()方法
signal(): 喚醒此Lock對(duì)象上的單個(gè)線程。
signalAll():喚醒此Lock對(duì)象上的所有線程。

線程池

創(chuàng)建一個(gè)新的線程成本還是比較高的,因?yàn)樗婕傲顺绦蚺c操作系統(tǒng)之間的交互,所以使用線程池可以很好的提高性能,尤其是程序的線程的生命周期很短,需要頻繁的創(chuàng)建線程的時(shí)候。

Java5提供了內(nèi)建的線程池支持,新增了一個(gè)Executors工廠類來產(chǎn)生線程。

  • newCachedThreadPool():創(chuàng)建一個(gè)具有緩存功能的線程池,系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建線程,這些線程將會(huì)被緩存在線程池中 。
  • newFixedThreadPool(int nThreads): 創(chuàng)建一個(gè)可重用,固定線程數(shù)的線程池。
  • newSingleThreadPool():創(chuàng)建一個(gè)只有單線程的線程池

上面三個(gè)方法返回ExecutorService對(duì)象代表一個(gè)線程池,可以執(zhí)行Runnable對(duì)象或者Callable對(duì)象所代表的線程

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

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

線程池的簡(jiǎn)單使用:

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();
    }
}


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容