1. 程式人生 > 實用技巧 >關於Java多執行緒看這一篇就夠了,從建立執行緒到執行緒池分析的明明白白

關於Java多執行緒看這一篇就夠了,從建立執行緒到執行緒池分析的明明白白

前言

程序是指一個記憶體中執行的應用程式,每個程序都有自己獨立的一塊記憶體空間,即程序空間或(虛空間)。程序不依賴於執行緒而獨立存在,一個程序中可以啟動多個執行緒。
執行緒是指程序中的一個執行流程,一個程序中可以執行多個執行緒。執行緒總是屬於某個程序,執行緒沒有自己的虛擬地址空間,與程序內的其他執行緒一起共享分配給該程序的所有資源,統一程序內的執行緒共享一個堆記憶體,每個執行緒具有自己的棧記憶體。“同時”執行是人的感覺,線上程之間實際上輪換執行。

同步與非同步

同步:排隊執行,效率低但是安全。
非同步:同步執行,效率高但是資料不安全。

併發與並行

併發:指兩個或多個事件在同一時間段內發生。
並行:指兩個或多個事件在同一時刻發生(同時發生)。

執行緒建立的3種方式

1.繼承方法

public  class MyThread extends Thread{
    @Override
    public void run(){
    }
}

        MyThread m = new MyThread();
        m.start();

2.介面方法

        //實現runnable
        //1 建立一個任務物件
        MyRunnable r = new MyRunnable();
        //建立一個執行緒並給他一個任務
        Thread t = new Thread(r);
        //啟動執行緒
        t.start();

介面的優勢:

實現Runnable與繼承Thread相比有如下優勢
1.通過建立任務,然後給執行緒分配任務的方式實現多執行緒,更適合多個執行緒同時執行任務的情況
2,可以避免單繼承所帶來的侷限性
3,任務與執行緒是分離的,提高了程式的健壯性
4,後期學習的執行緒池技術,接受Runnable型別的任務,不接受Thread型別的執行緒

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("12345"+i);
                }
            }
        }.start();

但是繼承Thread有個很簡單的實現方式,通過匿名內部類重寫run()不用重新建立一個類而簡單的實現了多執行緒,每個執行緒都有自己的棧空間,而共用一個堆記憶體。
由一個執行緒呼叫的方法,方法指揮執行在此執行緒中。

3.Callable實現執行緒的狀態的返回(實現了Callalble介面)

Callalble介面支援返回執行結果,需要呼叫FutureTask.get()得到,此方法會阻塞主程序的繼續往下執
行,如果不呼叫不會阻塞。

	Callable<Integer> callable= new MyCallable();
	FutureTask<Integer> future = new FutureTask<>(callable); 
	new Thread(future).start();
	Integer j=task.get();
	System.out.println("return"+j);

  1. 編寫類實現Callable介面 , 實現call方法
class Mycallable implements Callable<T> { 
	@Override 
	public <T> call() throws Exception { 
	return T; 
	} 
} 

  1. 建立FutureTask物件 , 並傳入第一步編寫的Callable類物件 FutureTask future = new FutureTask<>(callable);
  2. 通過Thread,啟動執行緒 new Thread(future).start();

如果呼叫get方法,則主執行緒等待其執行完成之後再執行,如果不呼叫則對主執行緒無影響可並行。

FutureTask()方法

其父類Future()方法

守護執行緒與使用者執行緒

執行緒分為守護執行緒和使用者執行緒
使用者執行緒:當一個程序不包含任何的存活的使用者執行緒時,進行結束
守護執行緒:守護使用者執行緒的,當最後一個使用者執行緒結束時,所有守護執行緒自動死亡。
java預設執行緒為使用者執行緒,通過執行緒呼叫setDaemon(true)設定守護執行緒。

        Thread t1 = new Thread(new MyRunnable());
        //設定守護執行緒
        t1.setDaemon(true);
        t1.start();

執行緒同步的三種方法

1.同步程式碼塊,指定鎖比如同一個object

格式:synchronized(鎖物件){}

public class Demo8 {
    public static void main(String[] args) {
        Object o = new Object();
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //總票數
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
                while (true) {
                    synchronized (o) {
                        if (count > 0) {
                         //賣票
                            System.out.println("正在準備賣票");
                            try {
                            Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            e.printStackTrace();
                            }
                            count--;
                            System.out.println(Thread.currentThread().getName()+"賣票結束,餘票:" + count);
                        }else {
                            break;
                        }

                }
            }
        }
    }
}

2. 同步方法

run()裡面呼叫同步方法,如果是普通方法使用方法內部的this(此物件)作為鎖,如果是靜態方法是類名.class(位元組碼檔案物件)

        public synchronized boolean test(){
                System.out.println("測試");
            }

3.顯示鎖

同步程式碼塊和同步方法都是隱式鎖。
顯式鎖更加直觀,體現面向物件,自己生成鎖,自己加鎖解鎖。

    static class Test implements Runnable{
        //總票數
        //引數為true表示公平鎖    預設是false 不是公平鎖
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {
            l.lock();
            System.out.println("測試");
            l.unlock();
        }
    }

公平鎖與不公平鎖

公平鎖:先來先到,一起排隊
不公平鎖:大家一起搶
java預設都是不公平鎖,可以通過顯式鎖中的構造方法實現公平鎖。

//設定公平鎖
private Lock l= new ReentrantLock(true);

不傳引數預設false不公平,傳入引數則公平

死鎖的避免

在任何有可能導致鎖產生的方法裡,不要呼叫另外一個有可能產生鎖的方法讓另外一個鎖產生。
下面是死鎖產生的例子:

public class Demo11 {
    public static void main(String[] args) {
        //執行緒死鎖
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);
    }

    static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);
        }
    }
    static class Culprit{
        public synchronized void say(Police p){//兩方法公用this(此物件)鎖,第一個方法不執行完無法執行第二個方法
            System.out.println("罪犯:你放了我,我放了人質");
            p.fun();
        }
        public synchronized void fun(){
            System.out.println("罪犯被放了,罪犯也放了人質");
        }
    }
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人質,我放了你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救了人質,但是罪犯跑了");
        }
    }
}

此時為死鎖

若第一個執行緒執行(假設)p.fun()時,第二個執行緒p還沒執行p.say()方法,此時沒鎖到,兩方法都能執行,但出現錯誤。

生產者與消費者

1.一個類,可以修改可以讀取,如果不使用執行緒同步,在修改時發生了另一執行緒的讀取操作,會發生讀取髒資料的情況。
2.通過加入synchronized執行緒同步方法,修改時無法讀取可以解決讀取髒資料的問題,但是無法實現兩執行緒之間的同步,會發生一個執行緒的搶佔時間片現象無法同步。
3.如果想要讀取和修改可以互相等待同步,還需要加入wait和notify操作,一個執行完notifyall休息,置判別標識,另一個被喚醒後讀取判別標識執行然後休息,在喚醒另一個。加入flag標誌判斷執行緒執行先後。

package com.java.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo4  {

    /**
     * 多執行緒通訊問題, 生產者與消費者問題
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }

    //廚師
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndSaste("老乾媽小米粥","香辣味");
                }else{
                    f.setNameAndSaste("煎餅果子","甜辣味");
                }
            }
        }
    }
    //服務生
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;

        //true 表示可以生產
        private boolean flag = true;

        public synchronized void setNameAndSaste(String name,String taste){
            if(flag) {
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if(!flag) {
                System.out.println("服務員端走的菜的名稱是:" + name + ",味道:" + taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行緒的狀態

執行緒狀態。 執行緒可以處於以下狀態之一:
NEW
尚未啟動的執行緒處於此狀態。
RUNNABLE
在Java虛擬機器中執行的執行緒處於此狀態。
BLOCKED
被阻塞等待監視器鎖定的執行緒處於此狀態。
WAITING
無限期等待另一個執行緒執行特定操作的執行緒處於此狀態。
TIMED_WAITING
正在等待另一個執行緒執行最多指定等待時間的操作的執行緒處於此狀態。
TERMINATED
已退出的執行緒處於此狀態。
執行緒在給定時間點只能處於一種狀態。 這些狀態是虛擬機器狀態,不反映任何作業系統執行緒狀態。

執行緒池

1.快取執行緒池

無限制長度
任務加入後的執行流程:
1.判斷執行緒池是否存在空閒執行緒
2.存在則使用
3.不存在則建立執行緒並使用


        ExecutorService service = Executors.newCachedThreadPool();
        //指揮執行緒池執行新的任務
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"測試");
            }

        });

2.定長執行緒池

長度是指定的執行緒池
加入任務後的執行流程:
1 判斷執行緒池是否存在空閒執行緒
2 存在則使用
3 不存在空閒執行緒 且執行緒池未滿的情況下 則建立執行緒 並放入執行緒池中 然後使用
4 不存在空閒執行緒 且執行緒池已滿的情況下 則等待執行緒池的空閒執行緒

		//設定定長執行緒池
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"測試");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

3.單執行緒執行緒池

效果與長度為1的定長執行緒池一樣。
執行流程
1 判斷執行緒池的那個執行緒是否空閒
2 空閒則使用
3 不空閒則等待它空閒後再使用

        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"測試");
            }
        });

4.週期定長執行緒池

執行流程
    1 判斷執行緒池是否存在空閒執行緒
    2 存在則使用
    3 不存在空閒執行緒  且執行緒池未滿的情況下  則建立執行緒  並放入執行緒池中  然後使用
    4 不存在空閒執行緒  且執行緒池已滿的情況下  則等待執行緒池的空閒執行緒

週期性任務執行時:
定時執行 當某個任務觸發時 自動執行某任務

執行一次:

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //定時執行一次
        //引數1:定時執行的任務
        //引數2:時長數字
        //引數3:2的時間單位    Timeunit的常量指定
       	scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"測試");
            }
        },5, TimeUnit.SECONDS);      //5秒鐘後執行*/

週期執行

        週期性執行任務
            引數1:任務
            引數2:延遲時長數字(第一次在執行上面時間以後)
            引數3:週期時長數字(沒隔多久執行一次)
            引數4:時長數字的單位
        * **/
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"測試");
            }
        },5,1,TimeUnit.SECONDS);

Lambda表示式

再使用匿名內部類作為引數時,可以使用lambda寫法來極大的簡化程式碼。

        //冗餘的Runnable編寫方式
       Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("測試");
            }
        });
        t.start();

保留傳遞的引數,保留要重寫的方法體,中間用-> 連線

        Thread t = new Thread(() -> System.out.println("測試"));

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!