Java多線程

線程多線程概念、兩種實現方式的區別進程與線程

從計算機操作系統的發展來看,經歷了這樣的兩個階段

單線程處理:最傳統的DOS系統中只要有病毒出現,則立即有反映,因爲在DOS系統中屬於單進程處理,即:在同一個時間段上只有一個程序在執行。

多線程處理:windows操作系統是一個多進程,例如,假設在windows中出現病毒了,則系統一樣可以使用

進程:正在進行中的程序。

1.png

那麼對於資源來講,所有的IO設備、CPU等等只有一個,那麼對於多線程處理來講,在同一個時間段上會有多個程序運行,但是在同一個時間點上只能有一個程序運行。

線程是在進程基礎上的進一步劃分,例子:word中的拼寫檢查,是在word整個程序

運行中運行 word是一個進程拼寫檢查可以當作一個線程,一個進程可以有多個線程,就類似於,開了一個遊戲,遊戲看做一個進程,遊戲裏面的玩家看作線程,遊戲沒有肯定玩家就沒了,但是遊戲裏面玩家沒了,遊戲卻還在。所以說線程是在進程基礎上進一部劃分

所以,如果進程消失了,則線程消失了,而如果線程消失的話,則進程依然會執行,未必會消失,java本身是屬於多線程的操作語言,所以提供了線程的處理機制。

一個進程默認一個線程(main);

進程和線程切換,誰耗內存。

進程消耗性能一些,線程是在進程中切換不耗內存。

線程是CPU的最小執行單元。

所謂的線程是一組指令的集合。

線程實現的兩種方式

java中可以有兩種方式實現多線程操作,一種是是繼承Thread類,另外一種是實現Runnable 接口。

Thread是在java.lang包中定義的。

一個類只要繼承了Thread類,同時覆寫了本類中的run()方法 ,則就可以實現多線程的操作了。

package com.cym.thread;

public class MyThread extends Thread {

    private String name;

    public MyThread(String name){

        this.name = name;

    }

    public void run(){

        for (int i = 0; i < 10; i++) {

            System.out.println(name + ":" + i);

        }

    }

}

以上的類已經完成了多線程的操作類,那麼下面就啓動線程

package com.cym.thread;

public class Main {

    public static void main(String[]args) {

        // TODO Auto-generated method stub

        MyThreadmt1 = new MyThread("A");

        MyThreadmt2 = new MyThread("B");

        mt1.run(); // 執行線程體

        mt2.run(); // 執行線程體

        

    }

}

但是、此時執行可以發現非常的有規律,先執行玩第一個對象的,再執行第二個對象,也就是說並沒有現實交互的運行。
    
JDK文檔可以發現,一旦調用start()方法,則會通過JVM找到run()方法

publicvoid start()

使用start啓動線程

package com.cym.thread;

public class Main {

    public static void main(String[]args) {

        // TODO Auto-generated method stub

        MyThread mt1 = new MyThread("A");

        MyThread mt2 = new MyThread("B");

        mt1.start(); // 執行線程體

        mt2.start(); // 執行線程體

        

    }

}

此時程序已經可以進程交互式的運行了。

但是需要思考的是爲什麼要使用start()方法使用啓動多線程?

JDK的安裝路徑下,src.zip是全部的java源程序,通過此代碼找到Thread 類中的start()方法定義 :

  public synchronized void start() { // 定義的start()方法

     

       if (started) // 判斷線程是否已經啓動

           throw new IllegalThreadStateException();

       Stared = true; // 如果沒有啓動則修改狀態

start0();  // 使用native 關鍵字聲明的的方法,沒有方法體.

       if (stopBeforeStart) {

           stop0(throwableFromStop);

       }

    }

private native void start0(); // 使用native 關鍵字聲明的方法,沒有方法體。

操作系統有很多,Windows、Linux、UNLX,既然多線程操作中要進行CPU資源的搶佔,也就是說要等待CPU的調度,那麼這些調度的操作是由各種操作系統的低層實現的,

所以在java程序中根本就沒法實現,那麼此時java的設計者定義了native關鍵字,使用此

關鍵字表示可以調用操作系統的底層函數,那麼這種技術又稱作爲JNI技術(java nativeinterface

而且,此方法在執行的時候將調用run()方法完成,有系統默認調用的。

但是第一中操作中有一個最大的限制,一個類只能繼承一個父類。

Runnable接口

在實際的開發中一個多線程的操作類很少去使用thread類完成,而是通過Runnable接口完成。

觀察runnable接口定義

publicinterface Runnable

Public void run();

如果說一個類要實現此接口那麼,就要實現run()方法;

package com.cym.runnable;

public class MyRunnable implements Runnable {

    private String name;

    public MyRunnable(String name){

        this.name = name;

    }

    public void run(){

        for (int i = 0; i < 100; i++) {

            System.out.println(name + ":" + i);

        }

    }

}

完成之後,下面啓動多線程

但是在現在使用Runnable的定義中並沒有start()方法,而只有Thread 類中才有.

Thread類中存以下的一個構造方法:

publicThread(Runnable target)

      此構造方法接收Runnable的子類實例,也就是說現在可以通過Thread 類來啓動Runnable現實的多線程,

package com.cym.runnable;

public class Main {

    public static void main(String[]args) {

        // TODO Auto-generated method stub

        MyRunnable mr1 = new MyRunnable("A");

        MyRunnable mr2 = new MyRunnable("B");

        new Thread(mr1).start(); // 執行線程體

        new Thread(mr2).start(); // 執行線程體

        

    }

}

以上的操作代碼也屬於交替的運行,所以此時程序也實現了多線程的操作。

兩種方式的區別及練習

在程序的開發中要是多線程則肯定永遠以實現Runnable接口爲正統操作,因爲實現Runnable接口相比繼承Thread類有如下好處:

1避免了繼承的侷限,一個類可以同時現實多個接口。

2適合於資源共享

以買票的程序爲例,通用Thread類完成

package com.cym.threadticket;

public class MyThread extends Thread {

    private int ticket = 5;

     

    public void run(){

        for (int i = 0; i < 100; i++) {

            if(this.ticket > 0){

            System.out.println("買票" + this.ticket--);

            }

        }

    }

}

下面建立三個線程同時賣票

package com.cym.threadticket;

public class Main {

    public static void main(String[]args) {

        // TODO Auto-generated method stub

        MyThread mt1 = new MyThread(); // 一個線程

        MyThread mt2 = new MyThread(); // 一個線程

        MyThread mt3 = new MyThread(); // 一個線程

        mt1.start(); // 開始買票

        mt2.start(); // 開始買票

        mt3.start(); // 開始買票

        

    }

}

發現一共買了15張票,但是實際上只有5張票,所以證明每一個線程都賣自己的票沒有達到資源共享的目的。

如果我們通過實現Runnable接口的話,那麼我們就可以現實資源共享的目的

package com.cym.runnableticket;

public class MyRunnable implements Runnable {

    private int ticket = 5;

     

    public void run(){

        for (int i = 0; i < 100; i++) {

            if(this.ticket > 0){

            System.out.println("買票" + this.ticket--);

            }

        }

    }

}

編寫多個線程進行賣票

package com.cym.runnableticket;

public class Main {

    public static void main(String[]args) {

        // TODO Auto-generated method stub

        MyRunnable mr1 = new MyRunnable();

        new Thread(mr1).start(); // 買票

        new Thread(mr1).start(); // 買票

        new Thread(mr1).start(); // 買票

        

    }

}

雖然現在程序中有三個線程,但是一共才賣出去5張票。也就是說使用Runnable現實的多線程可以達到資源共享的目的。

實際上Runnable接口和Thread類還是有聯繫的

publicclass Thread

extendsObject

implementsRunnable

發現thread類也是實現了Runnable

一個接口同時有兩個子類(假設是A,B),把A類放在B類之中,那麼B類完成更多的功能但是最終結構還是以A類爲準

這是一個典型的代理設計。

2.png

線程的操作方法

對於線程來講所有的操作方法都在Thread類中定義的,所以如果想明確的理解操作方法,則肯定要從Thread類着手。

設置和取得名字

在Thread類中存在以下的幾個方法可以設置和取得名字,

設置名字:

set:public finalvoid setName(String name);

構造:

public Thread(Runnable target, String name);

public Thread(String name);

取得名字:

public final StringgetName();

在線程的操作中以爲其操作的不確定性,所以提供了一個方法,可以取得當前的操作線程:

public static Thread currentThread()

對於線程的名字一般在啓動前進行設置,最好不要設置相同的名字,最好不要爲一個線程改名字。

Public class MyThread implements Runnable {

Public voidrun(){

For(int i = 0; i < 10; i++){

System.out.println(Thread.currentThread().getName + “線程正在運行。”);

}

}

}

那麼測試一下,上面是取得了線程的名字。

Main

MyThread mt =new MyThread() //  runnable子類實例

New Thread(mt,“線程A”).start();

New Thread(mt,“線程B”).start();

New Thread(mt,“線程C”).start();

明白代碼的作用之後,再看如下的程序:

Main

MyThread mt =new MyThread() //  runnable子類實例

New Thread(mt,“自定義線程”).start();

Mt.run(); // 直接通過對象調用

得出一個結論,在程序運行時主方法實際上就是一個主線程。

一直強調java是個多線程的操作語言,那麼它是如何實現的呢?

實際上對於java來講,每一次執行java命令對於操作系統來講都是啓動一個JVM的進程,

那麼主方法實際上只是這個進程上的進一步劃分。

問題:

在java執行一個java程序至少啓動幾個線程?

至少啓動兩個:main、gc

線程的休眠

讓一個線程稍微小小的休息一下,之後起來繼續工作,稱爲休眠:

public staticvoid sleep(long millis,int nanos) throws InterruptedException

在使用方法時需要try..catch處理

package com.cym.sleep;

public class MyThread implements Runnable {

    @Override

    public void run() {

        for (int i = 0; i < 10; i++) {

            try {

                Thread.sleep(500);

            } catch (InterruptedException e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName()+"線程正在運行" );

        }

    }

}

Main

    MyThread mt = new MyThread();

    new Thread(mt,"線程A").start();

new Thread(mt,"線程B").start();

程序的執行將變緩慢起來。

線程的中斷

在sleep()方法中存在InterruptException,那麼會造成此異常的方法就是中斷:

public void interrupt()

package com.cym.interrupt;

public class Main {

    public static void main(String[]args) {

        MyThread mt = new MyThread();

        Thread t = new Thread(mt,"自定義線程");

        t.start();

        t.interrupt(); // 中斷線程

    }

}

class MyThread implements Runnable {

    @Override

    public void run() {

        System.out.println("1進入run()");

            try {

                System.out.println("2準備睡眠20秒");

                Thread.sleep(2000);

                System.out.println("3睡眠20秒完成");

            } catch (InterruptedException e) {

                System.out.println("4中斷線程");

                return;// 返回方法調用處

            }   

            System.out.println(Thread.currentThread().getName()+ "線程正在執行");

     

        System.out.println("5退出run()");

    }

        

}

線程的優先級

在多線程的操作中,所有的代碼實際上都是存在優先級的,優先級高的就有可能先執行。在線程中使用以後的方法設置:public final void setPriority(int newPriority)

最高:public static final intMAX_PRIORITY

中等:public static final intNORM_PRIORITY

最低:public static final intMIN_PRIORITY

package com.cym.priority;

public class Main {

    public static void main(String[]args) {

        MyThread mt = new MyThread();

        Thread t1 = new Thread(mt,"A線程");

        Thread t2 = new Thread(mt,"B線程");

        Thread t3 = new Thread(mt,"C線程");

        t2.setPriority(Thread.MIN_PRIORITY);//設置成最低優先級

        t3.setPriority(Thread.MAX_PRIORITY);//設置最高優先級

        t1.setPriority(Thread.NORM_PRIORITY);//設置成中等優先級

        

        

         t1.start();

         t2.start();

         t3.start();

    }

}

class MyThread implements Runnable {

    @Override

    public void run() {

         for (int i = 0; i < 10; i++) {

             try {

                Thread.sleep(500);

            } catch (InterruptedException e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName()+"線程在運行");

        }

    }

        

}

各個線程可以設置優先級,那麼主方法的優先級呢?

package com.cym.priority;

public class TestPriorityMain {

    public static void main(Stringargs[]){

    System.out.println(Thread.currentThread().getPriority());//5

    System.out.println(Thread.MAX_PRIORITY); //10

    System.out.println(Thread.NORM_PRIORITY);//5

    System.out.println(Thread.MIN_PRIORITY);//1

    }

}

從輸出結果可以發現,主方法的優先級是普通的優先級

同步和死鎖(理解)問題的引出

package com.cym.syn;

class MyTickerTreadimplements Runnable{ // Runnable接口

    private int ticker = 5; // 一共才5張票

   

    public void run() { // 覆寫run()方法

        for (int i = 0; i < 100; i++) { // 表示循環10次

            if(ticker > 0){

                try {

                    Thread.sleep(300); //延遲

                } catch (InterruptedException e) {

                    // TODO Auto-generated catchblock

                    e.printStackTrace();

                } // 延遲

                System.out.println("賣票:ticker = " + this.ticker--);

            }

               

        }

        

    }

   

}

public class Main {

    public static void main(String[]args) {

        MyTickerTread mt = new MyTickerTread();

        new Thread(mt).start();

        new Thread(mt).start();

        new Thread(mt).start();

    }

}

最終結果:

賣票:ticker =5

賣票:ticker =4

賣票:ticker =3

賣票:ticker =2

賣票:ticker =1

賣票:ticker =0

賣票:ticker =-1

從運行結果上可以發現,程序中加入了延遲操作,所以在運行的最後出現了負數的情況,那麼爲什麼現在會產生這樣的問題?

從上面的操作代碼可以發現對於票數的操作步驟如下:

1判斷票數是否大於0,大於0則表示還有票可以賣

2如果票數大於0,則賣票出去

但是,在上面的操作代碼中,在第一步和第二步之間加入了延遲操作,那麼一個線程就有可能在還沒有對票數進行操作的之前,其他線程就已經講票數減少了,這樣一來就會出現票數爲負的情況。

class MyTickerTreadimplements Runnable{ // Runnable接口

    private int ticker = 5; // 一共才5張票

   

    public void run() { // 覆寫run()方法

        for (int i = 0; i < 100; i++) { // 表示循環10次

            if(ticker > 0){

                try {

                    Thread.sleep(300); //延遲 由於延遲的線程,使得第一個線程進來的時候,還沒開始--操作,第2個線程又進來了,也就是從5還沒--,第二個線程進來的時候又是5--

                } catch (InterruptedException e) {

                    // TODO Auto-generated catchblock

                    e.printStackTrace();

                } // 延遲

                System.out.println("賣票:ticker = " + this.ticker--);

            }

               

        }

        

    }

   

}

如果想解決這樣的問題,就必須同步,所謂的同步就是指多個操作在同一時間段內只能有一個線程進行,其他的線程要等待此線程要等待此線程完成之後纔可以繼續執行。

java中可以通過代碼同步方式進行代碼的加鎖操作,同步的現實有兩種方式

同步代碼塊

同步方法

同步代碼塊

使用synchronized關鍵字進行同步代碼塊的聲明,但是在使用此操作時必須明確的指出到底要鎖定的是那一個對象,一般都是以當前對象爲主:

Synchronized(對象){ // 一般都是將this進行鎖定
              需要同步的代碼

}

package com.cym.syn;

class MyTickerTreadimplements Runnable{ // Runnable接口

    private int ticker = 5; // 一共才5張票

   

    public void run() { // 覆寫run()方法

        for (int i = 0; i < 100; i++) { // 表示循環10次

            synchronized(this){ // 使用同步代碼塊

            if(ticker > 0){

                try {

                    Thread.sleep(300); //延遲

                } catch (InterruptedException e) {

                    // TODO Auto-generated catchblock

                    e.printStackTrace();

                } // 延遲

                System.out.println("賣票:ticker = " + this.ticker--);

            }

            }

        }

        

    }

   

}

public class Main {

    public static void main(String[]args) {

        MyTickerTread mt = new MyTickerTread();

        new Thread(mt).start();

        new Thread(mt).start();

        new Thread(mt).start();

    }

}

此時解決了同步的問題,但是在java中也可以通過同步方法解決這樣的問題.

package com.cym.syn;

class MyTickerTreadimplements Runnable{ // Runnable接口

    private int ticker = 5; // 一共才5張票

   

    public void run() { // 覆寫run()方法

        for (int i = 0; i < 100; i++) { // 表示循環10次

         

            sale();

         

        }

        

    }

    public synchronized void sale(){

        if(ticker > 0){

            try {

                Thread.sleep(300); //延遲

            } catch (InterruptedException e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            } // 延遲

            System.out.println("賣票:ticker = " + this.ticker--);

        }

    }

   

}

public class Main {

    public static void main(String[]args) {

        MyTickerTread mt = new MyTickerTread();

        new Thread(mt).start();

        new Thread(mt).start();

        new Thread(mt).start();

    }

}

此時,實際上就可以給出java中方法定義的完整格式了:

[訪問權限

[static][final][synchronized][abstract][native]

[返回值類型

方法名稱(參數列表)throws 異常1,異常2....{}

發現同步可以保證資源的完整性,但是過多的同步也會出現問題。

死鎖

在程序中過多的同步會產生死鎖的問題,那麼死鎖屬於程序運行的時候發生的一種特殊的狀態,本章只是演示一個簡單的操作代碼,只要觀察到死鎖最終的運行狀態即可

package com.cym.syn;

class Robber {

    public synchronized void say(Casualtyc) {

        System.out.println("搶劫");

        c.give();

    }

    public synchronized void give() {

        System.out.println("搶劫成功");

    }

}

class Casualty {

    public synchronized void say(Robber r){

        System.out.println("不給");

        r.give();

    }

    public synchronized void give() {

        System.out.println("報警成功");

    }

}

public class DeadLockDemo implements Runnable {

    private Robber r = new Robber();

    private Casualty c = new Casualty();

    public DeadLockDemo() {

        new Thread(this).start();

        try {

            Thread.sleep(300);

        } catch (InterruptedException e) {

            // TODO Auto-generated catchblock

            e.printStackTrace();

        }

        c.say(r);

        

    }

    public void run() {

        try {

            Thread.sleep(300);

        } catch (InterruptedException e) {

            // TODO Auto-generated catchblock

            e.printStackTrace();

        }

        r.say(c);

    }

    public static void main(String[]args) {

        new DeadLockDemo();

    }

}

在本塊中只需要記住一個概念即可:

多個線程共享同一資源的時候需要進行同步,但是過多的同步有可能會造成死鎖。

解決辦法有:以上講過的(同步,設置優先級)

生存者和消費者

在多線程中有一個最經典的操作案例 ----- 生存者和消費者,生存者不斷生產內容,但是消費者不斷取出內容。

基本實現

現在假設生產的內容都保存在info類中,則在生產者要有info類的引用,而消費者中也要存在info類的引用。

生產者應該不斷的生產信息、消費者不斷的取出,所以實現多線程的操作。

從代碼中可以發現一下幾點問題:

生成的內容有可能發生不一致的內容。

出現了重複取值和重複設置的問題

加入同步

爲了保證程序操作數據的完整性,此時,最好加入同步的操作,可以直接在info類中定義一個同步方法,專門完成設置和取得的內容。

此時可以保證數據的一致性,但是依然存在重取和重複設置的問題。那麼該如何去掉這些問題?

Object對線程的支持

3.png

Notify()notifyAll()

對於喚醒的操作有兩個:notify()、notifyAll()。一般來說,所以等待的線程會按照順序進行排列,如果現在使用了notufy()方法的話,則會喚醒第一個等待的線程執行,而如果使用了notifyAll()方法,則會喚醒所有的等待線程,那個線程的優先級高,那個線程就有可能先執行。

現在就利用Object以上方法解決線程的等待喚醒操作

爲什麼使用多線程?

1.耗時的操作使用線程,提高應用程序響應

2.並行操作時使用線程,如C/S架構的服務器端併發線程響應用戶的請求。

3.多CPU系統中,使用線程提高CPU利用率

4.改善程序結構。一個既長又複雜的進程可以考慮分爲多個線程,成爲幾個獨立或半獨
立的運行部分,這樣的程序會利於理解和修改。
線程的生命週期

1.線程的生命週期
線程是一個動態執行的過程,它也有一個從產生到死亡的過程。

(1)生命週期的五種狀態

新建(new Thread
當創建Thread類的一個實例(對象)時,此線程進入新建狀態(未被啓動)。
例如:Thread  t1=new Thread();

就緒(runnable
線程已經被啓動,正在等待被分配給CPU時間片,也就是說此時線程正在就緒隊列中排隊等候得到CPU資源。例如:t1.start();

運行(running
線程獲得CPU資源正在執行任務(run()方法),此時除非此線程自動放棄CPU資源或者有優先級更高的線程進入,線程將一直運行到結束。

堵塞(blocked
由於某種原因導致正在運行的線程讓出CPU並暫停自己的執行,即進入堵塞狀態。

正在睡眠:用sleep(long t) 方法可使線程進入睡眠方式。一個睡眠着的線程在指定的時間過去可進入就緒狀態。

正在等待:調用wait()方法。(調用motify()方法回到就緒狀態)

死亡(dead
當線程執行完畢或被其它線程殺死,線程就進入死亡狀態,這時線程不可能再進入就緒狀態等待執行。

自然終止:正常運行run()方法後終止

異常終止:調用stop()方法讓一個線程終止運行

線程的優缺點

多線程的優點很明顯,線程中的處理程序依然是順序執行,符合普通人的思維習慣,所以編程簡單。但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔(電腦程序開很多配置如果不好,電腦會很卡)。並且線程間的共享變量可能造成死鎖的出現。(死鎖以上說過)

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