1. 程式人生 > 實用技巧 >Java自學第8期——多執行緒

Java自學第8期——多執行緒

1、多執行緒:

作業系統支援同時執行多個任務,一個任務通常是一個程式,所有執行中的程式就是一個程序()。程式內部包含多個順序執行流,每個順序執行流就是一個執行緒

併發:兩個或者多個事件在同一個時間段內交替發生;
並行:兩個或者多個事件在同一時刻同時發生;

某時間段內巨集觀上有多個程式同時執行,在單cpu系統中,每一時刻只有單個程式在執行,但在微觀上cpu是交替執行他們的。
多cpu系統中,這些多工被分配到多個cpu上,這樣多個任務就是同時執行。

1.1 執行緒與程序

程序:程序是併發執行程式在執行過程中資源分配和管理的基本單位(資源分配的最小單位)。程序可以理解為一個應用程式的執行過程,
應用程式一旦執行,就是一個程序。每個程序都有自己獨立的地址空間,每啟動一個程序,
系統就會為它分配地址空間,建立資料表來維護程式碼段、堆疊段和資料段。
執行緒:程式執行的最小單位。一個程序至少有一個執行緒,擁有多執行緒的應用程式被稱為多執行緒程式。

1.2 執行緒排程

分時排程:所有執行緒輪流使用cpu,平均分配每個orffff執行緒佔用cpu的時間
搶佔式排程:優先讓優先順序高的執行緒使用cpu,優先順序相同則隨機選擇。java使用搶佔式排程。
設定執行緒優先順序:工作管理員開啟詳細資訊,右鍵設定執行緒優先順序

1.3 建立執行緒類

java.lang.Thread類代表執行緒,所有執行緒類都必須是Thread類或者是其子類的例項,每個執行緒的作用是
完成一定的任務,即是執行一段程式流即一段順序執行的程式碼,java使用執行緒執行體來代表這段程式流。
通過繼承Thread類來建立並啟動多執行緒:
1.定義Thread的子類,重寫run()方法,其方法體代表執行緒執行的任務,該run()方法叫執行緒執行體、
2.建立子類的物件
3.呼叫執行緒物件的start()方法來啟動該執行緒

//定義測試類
public class Demo01 {
    public static void main(String[] args){
        Demo02 thread1 = new Demo02("執行緒1");
        thread1.run();
    }
}
//定義自定義執行緒類
class Demo02 extends Thread{

    //定義指定執行緒名稱的構造方法
    public Demo02(String name){
        //呼叫父類的String引數的構造方法,指定執行緒的名稱
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println(getName()+" is runnimg "+i);
        }
    }
}

2、瞭解Thread類

java.lang.Thread
構造方法:
public Thread() :分配一個新的執行緒物件。
public Thread(String name) :分配一個指定名字的新的執行緒物件。
public Thread(Runnable target) :分配一個帶有指定目標新的執行緒物件。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的執行緒物件並指定名字。
常用方法:
public String getName() :獲取當前執行緒名稱。
public void start() :導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法。
public void run() :此執行緒要執行的任務在此處定義程式碼。
public static void sleep(long millis) :使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行)。
public static Thread currentThread() :返回對當前正在執行的執行緒物件的引用。

2.1 建立執行緒的方式有兩種:

一種是繼承Thread類,另一種是實現Runnable介面方式
推薦實現Runnable介面:
1.定義該介面的實現類,並重寫該介面的run()方法,即為執行緒的執行體
2.建立該實現類的例項,並以該例作為Thread的target來建立Thread執行緒物件,該物件為真正的執行緒物件
3.呼叫執行緒物件的start()方法啟動執行緒

2.2 Thread類和Runnable類的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。
總結:
實現Runnable介面比繼承Thread類所具有的優勢:

  1. 適合多個相同的程式程式碼的執行緒去共享同一個資源。
  2. 可以避免java中的單繼承的侷限性。
  3. 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。
  4. 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類。
    擴充:在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用
    java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個進
    程。
public class Demo04 implements Runnable {//定義實現類

    //重寫run方法
    @Override
    public void run() {
        for (int i = 0; i <3 ; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }

}
class test1 {
    public static void main(String[] args) {
        //建立實現類物件
        Demo04 obj = new Demo04();
        //將該實現類物件給建立的執行緒物件,並賦名。參考Thread類的構造方法
        Thread thread1 = new Thread(obj,"執行緒");
        thread1.start();
    }
}

2.3匿名內部類方式實現執行緒的建立

使用該方式,可以方便的實現每個執行緒執行不同的任務操作
使用該方式實現Runnable介面:

public class Demo05 {
    public static void main(String[] args) {
//        new Runnable(){
//            @Override
//            public void run() {
//                for (int i = 0; i < 3; i++) {
//                    System.out.println(Thread.currentThread().getName()+i);
//                }
//            }
//        };
        Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };

        new Thread(r).start();

        for (int i = 0; i <4 ; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

後面學習使用lambda表示式優化寫法。

3、執行緒安全問題

執行緒安全:多個執行緒同時執行,執行同一段程式碼時,如果程式每次執行結果和單執行緒執行的結果是一樣的,同時其他變數的值和預期也是一樣。

3.1 執行緒同步

要解決多執行緒併發訪問同一資源可能出現的安全問題。可通過同步機制來解決(synchronized)
要求在某個執行緒修改共享資源的時候,其他執行緒不能修改該資源,等待修改完畢且同步後,
才能去搶奪cpu資源,完成對應的操作,保證了資料的同步性,從而保證執行緒安全。

有三種方法完成同步操作:
1、同步程式碼塊
2、同步方法
3、鎖機制

同步程式碼塊:
synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問
執行緒開始執行同步程式碼塊之前,必須先獲得同步監視器的鎖定。任何時刻只能有一個執行緒可以獲得對同步監視器的鎖定,當同步程式碼塊執行完成後,該執行緒會釋放對該同步監視器的鎖定。

格式:
synchronized(同步鎖/同步監視器){
需要同步操作的程式碼
}

同步鎖:
理解為給物件上鎖,
1、鎖物件,可以是任意型別
2、多個執行緒物件,要使用同一把鎖

注意:任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他執行緒只能在外等著。

public class Demo07 implements Runnable{
    private int num = 10;
    //建立lock
    Object lock = new Object();

    @Override
    public void run() {
        while(true) {//一直執行
            //新增同步鎖
            synchronized(lock) {
                if (num > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "數字為:" + num--);
                }
            }
        }
    }
}
class test2{
    public static void main(String[] args) {
        Demo07 x = new Demo07();
        Thread a = new Thread(x,"執行緒a");
        Thread b = new Thread(x,"執行緒b");
        Thread c = new Thread(x,"執行緒c");
        a.start();
        b.start();
        b.start();
    }
}

此時列印的內容已經沒有重複的出現了,已經執行緒安全

同步方法:
概念:使用synchronized修飾的方法,叫做同步方法,保證a執行緒執行該方法的時候,其他方法只能在外面等著。

格式:
public synchronized void method(){
可能會產生執行緒安全問題的程式碼
}

同步鎖,
對於非static方法,同步鎖就是this,
對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)

public class Demo08 implements Runnable {
    private int num = 10;

    @Override
    public void run() {
        while(true){//一直執行
            method1();
        }
    }

//同步方法
    public synchronized void method1(){
        if (num>0){
            try{
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //獲取當前執行緒的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"數字為:"+num--);
        }
    }
}

3.2 Lock鎖

java.util.concurrent.locks.Lock機制提供了比synchronized程式碼塊和synchronized方法更加廣泛的鎖定操作,同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向物件。
Lock鎖又稱同步鎖,加鎖與釋放鎖方法化了,如下:

public void lock():加同步鎖。
public void unlock():釋放同步鎖。

public class Demo09 implements Runnable {
    private int num = 10;
    //建立lock
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {//一直執行
            //新增同步鎖
            lock.lock();
            if (num > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //釋放同步鎖,finally確保一定會執行
                    lock.unlock();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "數字為:" + num--);
            }
        }
    }
}
class test5{
    public static void main(String[] args) {
        Demo09 obj = new Demo09();
        Thread thread = new Thread(obj,"執行緒x");
        thread.start();
    }
}

4、執行緒狀態

當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中,有幾種狀態呢?在API中java.lang.Thread.State 這個列舉中給出了六種執行緒狀態:
Object中兩個方法 wait()和notify()方法,

1.NEW(新建) :
執行緒剛被建立,但是並未啟動。還沒呼叫start方法。

2.Runnable(可執行):
執行緒可以在java虛擬機器中執行的狀態,可能正在執行自己程式碼,也可能沒有,這取決於操
作系統處理器。

3.Blocked(鎖阻塞):
當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀
態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態。

4.Waiting(無限等待):
一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個
狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。

5.Timed Waiting(計時等待):
同waiting狀態,有幾個方法有超時引數,呼叫他們將進入Timed Waiting狀態。這一狀態
將一直保持到超時期滿或者接收到喚醒通知。帶有超時引數的常用方法有Thread.sleep 、
Object.wait。

6.Teminated(被終止):
因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

4.1 Timed Waiting(計時等待)

一個正在限時等待另一個執行緒執行一個(喚醒)動作的執行緒處於這一狀態。在run方法中新增sleep語句,就強制當前正在執行的執行緒休眠(暫停執行),以減慢執行緒。
當我們呼叫了sleep方法之後,當前正在執行是執行緒進入休眠狀態。

1.呼叫sleep方法,單獨的執行緒可以呼叫,不一定非要有協作關係
2.為了讓其他執行緒有機會執行,可以將Thread.sleep()呼叫放線上程run()之內。以保證
該執行緒執行過程中會睡眠。
3.sleep與鎖無關,執行緒睡眠到期自動甦醒,並返回到Runnable(可執行狀態)
4.sleep中指定的時間是執行緒暫停執行的最短時間,不能保證時間到期後立即就會執行

4.2 BLOCKED(鎖阻塞):

一個正在阻塞等待一個監視器鎖(鎖物件)的執行緒處於這一狀態。
執行緒a和執行緒b使用同一個鎖,如果執行緒a獲取到鎖,則進入Runnable狀態,那麼執行緒b進入
到Blocked鎖阻塞狀態(即b沒有爭取到鎖物件)。

public class Demo10 extends Thread {
    //實現一個計數器,計數到100,
    // 在每個數字之間暫停1秒,每隔10個數字輸出一個字串
    public void run(){
        for (int i = 0;i < 100;i++){
            if ((i) % 10 == 0){
                System.out.println("——————" + i);
            }
            System.out.println(i);
            try{
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args){
        new Demo10().start();
    }
}

4.3 Waiting(無限等待)

一個正在無限期等待另一個執行緒執行一個特別的(喚醒)動作的執行緒處於這一狀態。
Onject中的兩個方法:

void wait():在其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法前,讓當前執行緒等待。

void notify():喚醒此物件監視器上等待的單執行緒,繼續執行wait之後的程式碼

void notify():喚醒所有正在等待的執行緒

public class Demo11 {
    //建立鎖物件,保證唯一
    public static Object obj = new Object();

    public static void main(String[] args) {
        //演示Waiting
        //
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    //新增同步鎖,保證只有一個執行緒執行
                    synchronized (obj) {
                        try {
                            System.out.println(Thread.currentThread().getName() + "獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件");
                            obj.wait();//無限等待

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "===從waiting狀態醒來,獲取到鎖物件,繼續執行了");
                    }
                }
            }
        }, "等待執行緒").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
//                while(true){

                try {
                    System.out.println(Thread.currentThread().getName() + "——等待3秒鐘");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (obj) {
                    System.out.println(Thread.currentThread().getName() + "獲取到時鎖物件,呼叫notify方法。釋放鎖物件");
                    obj.notify();
                }
            }
        }, "喚醒執行緒").start();
    }
}

5、執行緒池:

如果避免頻繁啟動和銷燬執行緒(尤其是大量生命短的執行緒),造成系統性能下降,使用執行緒池的方法。

執行緒池建立大量空閒執行緒,當執行緒池接收建立的Runnable物件,便會將一個空閒的執行緒執行該物件的run()方法,執行結束後迴歸空閒狀態,暫時不進行銷燬。

執行緒池還能控制併發執行緒的數量,通過他的最大執行緒數目的引數。

java5後Executors工廠類生產執行緒池,靜態方法:

newFixedThreadPool(int nThreads):建立一個可重用的、具有固定執行緒數的執行緒池.
newScheduledThreadPool(int corePoolSize):建立一個執行緒池,指定延遲後執行。

java8新增:
newWorkStealingPool(in parallelism):
建立持有足夠的執行緒池來支援給定的並行級別,該方法還會使用多個佇列減少競爭,可以無引數,自動匹配當前可支援最高並行級別,ExecutorService代表儘快執行的執行緒池,提交Runnable物件則會盡快執行該任務。靜態方法:

Future<?> submit(Runnable task):
提交Runnable物件,執行緒池將在有空閒時執行該任務。run()執行完後返回null。

Future<>T submit(Runnable task, T result):
提交給一個Runnable物件給執行緒池,空閒時執行。run()方法結束後返回result。

用完後,呼叫該執行緒池的shutdown()方法,變不再接受新的任務,但會將之前提交的任務完成。全部完成後,池中所有執行緒會死亡。

呼叫shutdownNow()方法,則立刻停止所有正在執行的活動任務,暫停正在等待的任務,並返回等待執行的任務的列表。

總步驟
1、呼叫Executors類的靜態工廠方法建立一個ExecutorService物件,該物件代表一個執行緒池。
2、建立Runnable實現類,作為執行緒執行任務。
3、呼叫ExecutorService物件的submit()方法來提交Runnable例項。
4、當不想提交任何任務時,呼叫ExecutorService物件的submit()方法來結束執行緒池。

public class Demo12_ThreadPool {
    public static void main(String[] args) throws Exception{
        //建立一個固定6個執行緒的執行緒池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        Runnable obj1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "執行");
            }
        };
        Runnable obj2 = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "執行");
            }
        };
        pool.submit(obj1);
        pool.submit(obj2);
        //關閉執行緒池
        pool.shutdown();
    }
}

下一期記錄lambda表示式的使用方法。