多線程【Thread、線程建立】

第13天 多線程
今日內容介紹
 Thread
 線程建立
 線程安全
 線程狀態
今日學習目標
 可以描述Java中多線程運行原理
 可以使用繼承類的方式建立多線程
 可以使用實現接口的方式建立多線程
 可以說出實現接口方式的好處
 可以解釋安全問題的出現的緣由
 可以使用同步代碼塊解決線程安全問題
 可以使用同步方法解決線程安全問題
 可以說出線程5個狀態的名稱
第1章 多線程
1.1 多線程介紹
學習多線程以前,咱們先要了解幾個關於多線程有關的概念。
進程:進程指正在運行的程序。確切的來講,當一個程序進入內存運行,即變成一個進程,進程是處於運行過程當中的程序,而且每一個進程都具備必定獨立功能。java

線程:線程是進程中的一個執行單元,來完成進程中的某個功能程序員


負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序。
簡而言之:一個程序運行後至少有一個進程,一個進程中能夠包含多個線程編程

 

什麼是多線程呢?即就是一個程序中有多個線程在同時執行。
經過下圖來區別單線程程序與多線程程序的不一樣:
 單線程程序:即,如有多個任務只能依次執行。當上一個任務執行結束後,下一個任務開始執行。如,去網吧上網,網吧只能讓一我的上網,當這我的下機後,下一我的才能上網。
 多線程程序:即,如有多個任務能夠同時執行。如,去網吧上網,網吧可以讓多我的同時上網。安全

1.2 程序運行原理
 分時調度
全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間。
 搶佔式調度
優先讓優先級高的線程使用 CPU,若是線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。多線程

1.2.1 搶佔式調度詳解
大部分操做系統都支持多進程併發運行,如今的操做系統幾乎都支持同時運行多個程序。好比:如今咱們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,」感受這些軟件好像在同一時刻運行着「r。併發

實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對咱們的感受要快,看上去就是在同一時刻運行。
其實,多線程程序並不能提升程序的運行速度,但可以提升程序運行效率,讓CPU的使用率更高。jvm

1.3 主線程[單線程程序]
回想咱們之前學習中寫過的代碼,當咱們在dos命令行中輸入java空格類名回車後,啓動JVM,而且加載對應的class文件。虛擬機並會從main方法開始執行咱們的程序代碼,一直把main方法的代碼執行結束。若是在執行過程遇到循環時間比較長的代碼,那麼在循環以後的其餘代碼是不會被立刻執行的。以下代碼演示:編輯器

class Person{
    String name;
    Person(String name){
        this.name = name;
    }
    void music()    {
        for (int i=1;i<=20;i++ )        {
            System.out.println(name+"在聽第"+i+"首歌");
        }
    }
    void eat()    {
        for (int i=1;i<=20;i++ )        {
            System.out.println(name+"在吃第"+i+"口飯");
        }
    }

}

class ThreadDemo {
    public static void main(String[] args)  {
        Person p = new Person("寶寶");
        p.music();      
        p.eat();
        System.out.println("聽完歌吃完飯了,該睡覺了zzZZ~~~");
    }
}

若在上述代碼中music方法中的循環執行次數不少,這時在p.music();下面的代碼是不會立刻執行的,而且在dos窗口會看到不停的輸出」xx在吃第幾口飯」,這樣的語句。爲何會這樣呢?
緣由是:jvm啓動後,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之爲主線程(main線程)。當程序的主線程執行時,若是遇到了循環而致使程序在指定位置停留時間過長,則沒法立刻執行下面的程序,須要等待循環結束後可以執行。
那麼,可否實現一個主線程負責執行其中一個循環,再由另外一個線程負責其餘代碼的執行,最終實現多部分代碼同時執行的效果?
可以實現同時執行,經過Java中的多線程技術來解決該問題。ide

1.4 Thread類
該如何建立線程呢?經過API中搜索,查到Thread類。經過閱讀Thread類中的描述。Thread是程序中的執行線程。Java 虛擬機容許應用程序併發地運行多個執行線程。函數

 構造方法


 經常使用方法

繼續閱讀,發現建立新執行線程有兩種方法。
 一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。建立對象,開啓線程。run方法至關於其餘線程的main方法。
 另外一種方法是聲明一個實現 Runnable 接口的類。該類而後實現 run 方法。而後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。

1.5 建立線程方式一繼承Thread類
建立線程的步驟:
1 定義一個類繼承Thread。
2 重寫run方法。
3 建立子類對象,就是建立線程對象。
4 調用start方法,開啓線程並讓線程執行,同時還會告訴jvm去調用run方法。
 測試類

public class Demo01 {
    public static void main(String[] args{
        //建立自定義線程對象
        MyThread mt = new MyThread("新的線程!");
        //開啓新線程
        mt.start();
        //在主方法中執行for循環
        for (int i = 0; i < 10; i++) {
            System.out.println("main線程!"+i);
        }
    }
}

 自定義線程類

public class MyThread extends Thread {
    //定義指定線程名稱的構造方法
    public MyThread(String name) {
        //調用父類的String參數的構造方法,指定線程的名稱
        super(name);
    }
    /**      * 重寫run方法,完成該線程執行的邏輯      */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在執行!"+i);
        }
    }
}

思考:線程對象調用 run方法和調用start方法區別?
線程對象調用run方法不開啓線程,僅是對象調用方法。線程對象調用start開啓線程,並讓jvm調用run方法在開啓的線程中執行。

1.5.1 繼承Thread類原理
咱們爲何要繼承Thread類? 還要調用其的start方法開啓線程呢?
1.Thread類:Thread類用來描述線程,具有線程應該有功能。
2.那爲何不直接建立Thread類的對象呢而要繼承Thread類呢?以下代碼:
Thread t1 = new Thread();
t1.start();//這樣作沒有錯,開啓一個線程,由線程本身去調用run方法,那麼這個run方法就在新的線程中運行起來了,可是咱們直接建立Thread對象,調用start方法,該start調用的是Thread類中的run方法,而這個run方法沒有作什麼事情,更重要的是這個run方法中並無定義咱們須要讓線程執行的代碼。[java設計師認爲他們不知道程序員會用run方法執行什麼代碼,全部沒寫run方法]
3.爲何線程對象調用 start方法而不調用run方法呢?
建立線程的目的是什麼?
是爲了創建程序單獨的執行路徑,讓多部分代碼實現同時執行。也就是說線程建立並執行須要給定線程要執行的任務。
對於以前所講的主線程,它的任務定義在main函數中。自定義線程須要執行的任務都定義在run方法中。
因此想要一個新線程執行咱們的任務,必須先要開啓線程,調用start方法,再由線程自動去執行run方法,而不是咱們本身調用run方法
1.5.2 多線程的內存圖解
多線程執行時,到底在內存中是如何運行的呢?
以上個程序爲例,進行圖解說明:
多線程執行時,在棧內存中,其實每個執行線程都有一片本身所屬的棧內存空間。進行方法的壓棧和彈棧。

當執行線程的任務結束了,線程自動在棧內存中釋放了。可是當全部的執行線程都結束了,那麼進程就結束了。
1.5.3 獲取線程名稱
每開啓的線程都會有一個名字,那麼這些運行的線程的名字是什麼呢?
1.按照咱們對象面向對象的理解,你說Thread對象的名字,應該保存在哪?
-------- 確定在線程對象中,咱們查閱一下Thread的API就發現有獲取線程對象的方法

咱們能夠在使用線程對象 調用getName()獲取線程的名字
2.那麼主線程的名字咱們怎麼獲取?
主線程的線程對象是你建立的嗎? 很明顯不是,是JVM建立並啓動的
查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,咱們就能夠試試。


 Thread.currentThread()獲取當前線程對象
 Thread.currentThread().getName();獲取當前線程對象的名稱

 

class MyThread extends Thread {  //繼承Thread

    //複寫其中的run方法
    public void run(){
        for (int i=1;i<=20 ;i++ ){
            System.out.println(getName()+",i="+i);
        }
    }
}
class ThreadDemo {
    public static void main(String[] args)  {
        //建立兩個線程任務
        MyThread d1 = new MyThread();
        MyThread d2 = new MyThread();
        D1.start();//沒有開啓新線程, 在主線程調用run方法
        d2.start();//開啓一個新線程,新線程調用run方法
         //獲取主線程對象,再經過主線程對象獲取主線程的名字
              Thread.currentThread().getName();
    }
}
 

經過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1……
進行多線程編程時,不要忘記了Java程序運行是從主線程開始,main方法就是主線程的線程執行內容。

1.6 建立線程方式—實現Runnable接口
建立線程的另外一種方法是聲明實現 Runnable 接口的類。該類而後實現 run 方法。而後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。
爲什麼要實現Runnable接口,Runable是啥玩意呢?繼續API搜索。
查看Runnable接口說明文檔:Runnable接口用來指定每一個線程要執行的任務。包含了一個 run 的無參數抽象方法,須要由接口實現類重寫該方法。

 接口中的方法


 Thread類構造方法


建立線程的步驟。
一、定義類實現Runnable接口。
二、覆蓋接口中的run方法。。
三、建立Thread類的對象
四、將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。
五、調用Thread類的start方法開啓線程。
 代碼演示:

 

public class Demo02 {
    public static void main(String[] args{
        //建立線程執行目標類對象
        Runnable runn = new MyRunnable();
        //將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數
        Thread thread = new Thread(runn);
        Thread thread2 = new Thread(runn);
        //開啓線程
        thread.start();
        thread2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main線程:正在執行!"+i);
        }
    }
}

 自定義線程執行任務類

public class MyRunnable implements Runnable{

    //定義線程要執行的run方法邏輯
    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {
            System.out.println("個人線程:正在執行!"+i);
        }
    }
}

1.6.1 實現Runnable的原理和好處
程序設計遵循的原則:開閉原則,對修改關閉,對擴展開放,減小線程自己和任務之間的耦合性
第二種方式實現Runnable接口避免了單繼承的侷限性,因此較爲經常使用。實現Runnable接口的方式,更加的符合面向對象,線程分爲兩部分,一部分線程對象,一部分線程任務。
第一種方式繼承Thread類,線程對象和線程任務耦合在一塊兒。一旦建立Thread類的子類對象,既是線程對象,有又有線程任務
實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。

1.7 線程的匿名內部類使用
使用線程的內匿名內部類方式,能夠方便的實現每一個線程執行不一樣的線程任務操做。
 方式1:建立線程對象時,直接重寫Thread類中的run方法

        new Thread() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...X...." + x);
                }
            }
        }.start();

 方式2:使用匿名內部類的方式實現Runnable接口,從新Runnable接口中的run方法

        Runnable r = new Runnable() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...Y...." + x);
                }
            }
        };
        new Thread(r).start();

第2章 線程安全

2.1 線程安全
什麼是線程安全問題?好比單人和多人上廁所案例
當有多個線程在同時運行,這些線程同時運行一段代碼(即同一個任務,同一個run方法),操做同一個共享數據時,這時候可能就會出現線程的安全問題,即線程不安全的.

注意:若是是單線程,或者多線程操做的仍是不一樣數據,那麼通常是沒有問題的
 咱們經過一個案例,演示線程的安全問題:
電影院要賣票,咱們模擬電影院的賣票過程。假設要播放的電影是 「功夫熊貓3」,本次電影的座位共100個(本場電影只能賣100張票)。
咱們來模擬電影院的售票窗口,實現多個窗口同時賣 「功夫熊貓3」這場電影票(多個窗口一塊兒賣這100張票)
須要窗口,採用線程對象來模擬;須要票,Runnable接口子類來模擬
 測試類

public class ThreadDemo {
    public static void main(String[] args) {
        //建立票對象
        Ticket ticket = new Ticket();

        //建立3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

 模擬票

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    @Override
    public void run() {
        //模擬賣票
        while(true){
            if (ticket > 0) {
                //模擬選坐的操做
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }
} 


運行結果發現:上面程序出現了問題
 票出現了重複的票
 錯誤的票 0、-1

 

其實,線程安全問題都是由全局變量及靜態變量引發的。若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。

2.2 線程同步(線程安全處理Synchronized)
java中提供了線程同步機制,它可以解決上述的線程安全問題。
線程同步的方式有兩種:
 方式1:同步代碼塊
 方式2:同步方法

2.2.1 同步代碼塊(同步鎖)
同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {
    可能會產生線程安全問題的代碼
}

同步代碼塊中的鎖對象能夠是任意的對象;但多個線程時,要使用同一個鎖對象纔可以保證線程安全。

使用同步代碼塊,對電影院賣票案例中Ticket類進行以下代碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步代碼塊
            synchronized (lock){
                if (ticket > 0) {
                    //模擬電影選坐的操做
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}

當使用了同步代碼塊後,上述的線程的安全問題,解決了。
2.2.2 同步方法
 同步方法:在方法聲明上加上synchronized
public synchronized void method(){
可能會產生線程安全問題的代碼
}
同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行以下代碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步方法
            method();
        }
    }

//同步方法,鎖對象this
    public synchronized void method(){
        if (ticket > 0) {
            //模擬選坐的操做
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
}

 靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){
可能會產生線程安全問題的代碼
}
靜態同步方法中的鎖對象是 類名.class

2.3 Lock接口

查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可得到的更普遍的鎖定操做。
 Lock接口中的經常使用方法

class X {
private final ReentrantLock lock = new ReentrantLock();
public void run() {
lock.lock(); // block until condition holds
try {
// … method body
} finally {
lock.unlock()
}
}
}

Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操做鎖的功能。
咱們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行以下代碼修改:

```

public class Ticket implements Runnable {

//共100票
int ticket = 100;

//建立Lock鎖對象
Lock ck = new ReentrantLock();

@Override
public void run() {
    //模擬賣票
    while(true){
        //synchronized (lock){
        ck.lock();
            if (ticket > 0) {
                //模擬選坐的操做
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        ck.unlock();
        //}
    }
  }
}

2.4 線程狀態圖

若是咱們屢次調用線程對象的start方法,那麼久會出現一個異常,查閱API關於IllegalThreadStateException這個異常說明信息發現,這個異常的描述信息爲:指示線程沒有處於請求操做所要求的適當狀態時拋出的異常。這裏面說適當的狀態,啥意思呢?難道是說線程還有狀態嗎?
每一個線程是有本身狀態的,就比如人的一輩子從出生到死亡同樣,線程也有,具體狀態能夠查看Thread的一個內部枚舉Thread.State.

相關文章
相關標籤/搜索