1. 程式人生 > 實用技巧 >關於JAVA中的static方法、併發問題以及JAVA執行時記憶體模型

關於JAVA中的static方法、併發問題以及JAVA執行時記憶體模型

18.1 基本概念

18.1.1 程式和程序的概念

程式 - 資料結構 + 演算法,主要指存放在硬碟上的可執行檔案。

程序 - 主要指執行在記憶體中的可執行檔案。

目前主流的作業系統都支援多程序,為了讓作業系統同時可以執行多個任務,但程序是重量級的,也就是新建一個程序會消耗CPU和記憶體空間等系統資源,因此程序的數量比較侷限。

18.1.2 執行緒的概念

為了解決上述問題就提出執行緒的概念,執行緒就是程序內部的程式流,也就是說作業系統內部支援多程序的,而每個程序的內部又是支援多執行緒的,執行緒是輕量的,新建執行緒會共享所在程序的系統資源,因此目前主流的開發都是採用多執行緒。

多執行緒是採用時間片輪轉法來保證多個執行緒的併發執行,所謂併發就是指巨集觀並行微觀序列的機制。

18.2 執行緒的建立(重中之重)

18.2.1 Thread類的概念

java.lang.Thread類代表執行緒,任何執行緒物件都是Thread類(子類)的例項。

Thread類是執行緒的模板,封裝了複雜的執行緒開啟等操作,封裝了作業系統的差異性。

18.2.2 建立方式

自定義類繼承Thread類並重寫run方法,然後建立該類的物件呼叫start方法。

自定義類實現Runnable介面並重寫run方法,建立該類的物件作為實參來構造Thread型別的物件,然後使用Thread型別的物件呼叫start方法。

18.2.3 相關的方法

方法宣告

功能介紹

Thread()

使用無參的方式構造物件

Thread(String name)

根據引數指定的名稱來構造物件

Thread(Runnable target)

根據引數指定的引用來構造物件,其中Runnable是個介面型別

Thread(Runnable target, String name)

根據引數指定引用和名稱來構造物件

void run()

若使用Runnable引用構造了執行緒物件,呼叫該方法時最終呼叫介面中的版本

若沒有使用Runnable引用構造執行緒物件,呼叫該方法時則啥也不做

void start()

用於啟動執行緒,Java虛擬機器會自動呼叫該執行緒的run方法

18.2.4 執行流程

執行main方法的執行緒叫做主執行緒,執行run方法的執行緒叫做新執行緒/子執行緒。

main方法是程式的入口,對於start方法之前的程式碼來說,由主執行緒執行一次,當start方法呼叫成功後執行緒的個數由1個變成了2個,新啟動的執行緒去執行run方法的程式碼,主執行緒繼續向下執行,兩個執行緒各自獨立執行互不影響。

當run方法執行完畢後子執行緒結束,當main方法執行完畢後主執行緒結束。

兩個執行緒執行沒有明確的先後執行次序,由作業系統排程演算法來決定。

18.2.5 方式的比較

繼承Thread類的方式程式碼簡單,但是若該類繼承Thread類後則無法繼承其它類,而實現

Runnable介面的方式程式碼複雜,但不影響該類繼承其它類以及實現其它介面,因此以後的開發中推薦使用第二種方式。

public class SubRunnableRunTest {

    public static void main(String[] args) {

        // 1.建立自定義型別的物件,也就是實現Runnable介面類的物件
        SubRunnableRun srr = new SubRunnableRun();
        // 2.使用該物件作為實參構造Thread型別的物件
        // 由原始碼可知:經過構造方法的呼叫之後,Thread類中的成員變數target的數值為srr。
        Thread t1 = new Thread(srr);
        // 3.使用Thread型別的物件呼叫start方法
        // 若使用Runnable引用構造了執行緒物件,呼叫該方法(run)時最終呼叫介面中的版本
        // 由run方法的原始碼可知:if (target != null) {
        //                         target.run();
        //                    }
        // 此時target的數值不為空這個條件成立,執行target.run()的程式碼,也就是srr.run()的程式碼
        t1.start();
        //srr.start();  Error

        // 列印1 ~ 20之間的所有整數
        for (int i = 1; i <= 20; i++) {
            System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20
        }
    }
}

18.2.6 匿名內部類的方式

使用匿名內部類的方式來建立和啟動執行緒。

public class ThreadNoNameTest {

    public static void main(String[] args) {

        // 匿名內部類的語法格式:父類/介面型別 引用變數名 = new 父類/介面型別() { 方法的重寫 };
        // 1.使用繼承加匿名內部類的方式建立並啟動執行緒
        /*Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("張三說:在嗎?");
            }
        };
        t1.start();*/
        new Thread() {
            @Override
            public void run() {
                System.out.println("張三說:在嗎?");
            }
        }.start();

        // 2.使用實現介面加匿名內部類的方式建立並啟動執行緒
        /*Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("李四說:不在。");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();*/
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("李四說:不在。");
            }
        }).start();*/
        // Java8開始支援lambda表示式: (形參列表)->{方法體;}
        /*Runnable ra = ()-> System.out.println("李四說:不在。");
        new Thread(ra).start();*/

        new Thread(()-> System.out.println("李四說:不在。")).start();
    }
}

18.3 執行緒的生命週期(熟悉)

18.4 執行緒的編號和名稱(熟悉)

方法宣告

功能介紹

long getId()

獲取呼叫物件所表示執行緒的編號

String getName()

獲取呼叫物件所表示執行緒的名稱

void setName(String name)

設定/修改執行緒的名稱為引數指定的數值

static Thread currentThread()

獲取當前正在執行執行緒的引用

案例題目

自定義類繼承Thread類並重寫run方法,在run方法中先列印當前執行緒的編號和名稱,然後將執行緒的名稱修改為"zhangfei"後再次列印編號和名稱。

要求在main方法中也要列印主執行緒的編號和名稱。

public class ThreadIdNameTest extends Thread {

    public ThreadIdNameTest(String name) {
        super(name); // 表示呼叫父類的構造方法
    }

    @Override
    public void run() {
        System.out.println("子執行緒的編號是:" + getId() + ",名稱是:" + getName()); // 14  Thread-0 guanyu
        // 修改名稱為"zhangfei"
        setName("zhangfei");
        System.out.println("修改後子執行緒的編號是:" + getId() + ",名稱是:" + getName()); // 14  zhangfei
    }

    public static void main(String[] args) {

        ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
        tint.start();

        // 獲取當前正在執行執行緒的引用,當前正在執行的執行緒是主執行緒,也就是獲取主執行緒的引用
        Thread t1 = Thread.currentThread();
        System.out.println("主執行緒的編號是:" + t1.getId() + ", 名稱是:" + t1.getName());
    }
}

18.5 常用的方法(重點)

方法宣告

功能介紹

static void yield()

當前執行緒讓出處理器(離開Running狀態),使當前執行緒進入Runnable 狀態等待

static void sleep(times)

使當前執行緒從 Running 放棄處理器進入Block狀態, 休眠times毫秒, 再返回到Runnable如果其他執行緒打斷當前執行緒的Block(sleep), 就會發生

InterruptedException。

int getPriority()

獲取執行緒的優先順序

void setPriority(int newPriority)

修改執行緒的優先順序。

優先順序越高的執行緒不一定先執行,但該執行緒獲取到時間片的機會會更多一些

void join()

等待該執行緒終止

void join(long millis)

等待引數指定的毫秒數

boolean isDaemon()

用於判斷是否為守護執行緒

void setDaemon(boolean on)

用於設定執行緒為守護執行緒

案例題目

程式設計建立兩個執行緒,執行緒一負責列印1 ~ 100之間的所有奇數,其中執行緒二負責列印1 ~ 100之間的所有偶數。

在main方法啟動上述兩個執行緒同時執行,主執行緒等待兩個執行緒終止。

18.6 執行緒同步機制(重點)

18.6.1 基本概念

當多個執行緒同時訪問同一種共享資源時,可能會造成資料的覆蓋等不一致性問題,此時就需要對執行緒之間進行通訊和協調,該機制就叫做執行緒的同步機制。

多個執行緒併發讀寫同一個臨界資源時會發生執行緒併發安全問題。

非同步操作:多執行緒併發的操作,各自獨立執行。

同步操作:多執行緒序列的操作,先後執行的順序。

18.6.2 解決方案

由程式結果可知:當兩個執行緒同時對同一個賬戶進行取款時,導致最終的賬戶餘額不合理。

引發原因:執行緒一執行取款時還沒來得及將取款後的餘額寫入後臺,執行緒二就已經開始取款。

解決方案:讓執行緒一執行完畢取款操作後,再讓執行緒二執行即可,將執行緒的併發操作改為序列操作。

經驗分享:在以後的開發儘量減少序列操作的範圍,從而提高效率。

18.6.3 實現方式

在Java語言中使用synchronized關鍵字來實現同步/物件鎖機制從而保證執行緒執行的原子性,具體方式如下:

使用同步程式碼塊的方式實現部分程式碼的鎖定,格式如下: synchronized(類型別的引用) {

編寫所有需要鎖定的程式碼;

}

使用同步方法的方式實現所有程式碼的鎖定。

直接使用synchronized關鍵字來修飾整個方法即可該方式等價於:

synchronized(this) { 整個方法體的程式碼 }

import java.util.concurrent.locks.ReentrantLock;

public class AccountRunnableTest implements Runnable {
    private int balance; // 用於描述賬戶的餘額
    private Demo dm = new Demo();
    private ReentrantLock lock = new ReentrantLock();  // 準備了一把鎖

    public AccountRunnableTest() {
    }

    public AccountRunnableTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public /*synchronized*/ void run() {
        // 開始加鎖
        lock.lock();

        // 由原始碼可知:最終是account物件來呼叫run方法,因此當前正在呼叫的物件就是account,也就是說this就是account
        //synchronized (this) { // ok
        System.out.println("執行緒" + Thread.currentThread().getName() + "已啟動...");
        //synchronized (dm) { // ok
        //synchronized (new Demo()) { // 鎖不住  要求必須是同一個物件
            // 1.模擬從後臺查詢賬戶餘額的過程
            int temp = getBalance(); // temp = 1000  temp = 1000
            // 2.模擬取款200元的過程
            if (temp >= 200) {
                System.out.println("正在出鈔,請稍後...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("請取走您的鈔票!");
            } else {
                System.out.println("餘額不足,請核對您的賬戶餘額!");
            }
            // 3.模擬將最新的賬戶餘額寫入到後臺
            setBalance(temp); // balance = 800  balance = 800
        //}
        lock.unlock(); // 實現解鎖
    }

    public static void main(String[] args) {

        AccountRunnableTest account = new AccountRunnableTest(1000);
        //AccountRunnableTest account2 = new AccountRunnableTest(1000);
        Thread t1 = new Thread(account);
        Thread t2 = new Thread(account);
        //Thread t2 = new Thread(account2);
        t1.start();
        t2.start();

        System.out.println("主執行緒開始等待...");
        try {
            t1.join();
            //t2.start(); // 也就是等待執行緒一取款操作結束後再啟動執行緒二
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最終的賬戶餘額為:" + account.getBalance()); // 600  800
    }
}

class Demo{}
public class AccountThreadTest extends Thread {
    private int balance; // 用於描述賬戶的餘額
    private static Demo dm = new Demo(); // 隸屬於類層級,所有物件共享同一個

    public AccountThreadTest() {
    }

    public AccountThreadTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public /*static*/ /*synchronized*/ void run() {
        /*System.out.println("執行緒" + Thread.currentThread().getName() + "已啟動...");
        //synchronized (dm) { // ok
            //synchronized (new Demo()) { // 鎖不住  要求必須是同一個物件
            // 1.模擬從後臺查詢賬戶餘額的過程
            int temp = getBalance(); // temp = 1000  temp = 1000
            // 2.模擬取款200元的過程
            if (temp >= 200) {
                System.out.println("正在出鈔,請稍後...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("請取走您的鈔票!");
            } else {
                System.out.println("餘額不足,請核對您的賬戶餘額!");
            }
            // 3.模擬將最新的賬戶餘額寫入到後臺
            setBalance(temp); // balance = 800  balance = 800
        //}*/
        test();
    }

    public /*synchronized*/ static void test() {
        synchronized (AccountThreadTest.class) { // 該型別對應的Class物件,由於型別是固定的,因此Class物件也是唯一的,因此可以實現同步
            System.out.println("執行緒" + Thread.currentThread().getName() + "已啟動...");
            //synchronized (dm) { // ok
            //synchronized (new Demo()) { // 鎖不住  要求必須是同一個物件
            // 1.模擬從後臺查詢賬戶餘額的過程
            int temp = 1000; //getBalance(); // temp = 1000  temp = 1000
            // 2.模擬取款200元的過程
            if (temp >= 200) {
                System.out.println("正在出鈔,請稍後...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("請取走您的鈔票!");
            } else {
                System.out.println("餘額不足,請核對您的賬戶餘額!");
            }
            // 3.模擬將最新的賬戶餘額寫入到後臺
            //setBalance(temp); // balance = 800  balance = 800
        }
    }

    public static void main(String[] args) {

        AccountThreadTest att1 = new AccountThreadTest(1000);
        att1.start();

        AccountThreadTest att2 = new AccountThreadTest(1000);
        att2.start();

        System.out.println("主執行緒開始等待...");
        try {
            att1.join();
            //t2.start(); // 也就是等待執行緒一取款操作結束後再啟動執行緒二
            att2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最終的賬戶餘額為:" + att1.getBalance()); // 800

    }

    }

18.6.4 靜態方法的鎖定

當我們對一個靜態方法加鎖,如:

public synchronized static void xxx(){….} 那麼該方法鎖的物件是類物件。每個類都有唯一的一個類物件。獲取類物件的方式:類名.class。靜態方法與非靜態方法同時使用了synchronized後它們之間是非互斥關係的。原因在於:靜態方法鎖的是類物件而非靜態方法鎖的是當前方法所屬物件。

18.6.5 注意事項

使用synchronized保證執行緒同步應當注意: 多個需要同步的執行緒在訪問同步塊時,看到的應該是同一個鎖物件引用。 在使用同步塊時應當儘量減少同步範圍以提高併發的執行效率。

18.6.6 執行緒安全類和不安全類

StringBuffer類是執行緒安全的類,但StringBuilder類不是執行緒安全的類。

Vector類和 Hashtable類是執行緒安全的類,但ArrayList類和HashMap類不是執行緒安全的類。

Collections.synchronizedList() 和 Collections.synchronizedMap()等方法實現安全。

18.6.7 死鎖的概念

執行緒一執行的程式碼:

public void run(){

synchronized(a){ //持有物件鎖a,等待物件鎖b

synchronized(b){

編寫鎖定的程式碼;

}

}

}

執行緒二執行的程式碼:

public void run(){

synchronized(b){ //持有物件鎖b,等待物件鎖a

synchronized(a){

編寫鎖定的程式碼;

}

}

}

注意:

在以後的開發中儘量減少同步的資源,減少同步程式碼塊的巢狀結構的使用!

18.6.8 使用Lock(鎖)實現執行緒同步

(1)基本概念

從Java5開始提供了更強大的執行緒同步機制—使用顯式定義的同步鎖物件來實現。

java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。

該介面的主要實現類是ReentrantLock類,該類擁有與synchronized相同的併發性,在以後的執行緒安全控制中,經常使用ReentrantLock類顯式加鎖和釋放鎖。

(2)常用的方法

方法宣告

功能介紹

ReentrantLock()

使用無參方式構造物件

void lock()

獲取鎖

void unlock()

釋放鎖

(3)與synchronized方式的比較

Lock是顯式鎖,需要手動實現開啟和關閉操作,而synchronized是隱式鎖,執行鎖定程式碼後自動釋放。

Lock只有同步程式碼塊方式的鎖,而synchronized有同步程式碼塊方式和同步方法兩種鎖。

使用Lock鎖方式時,Java虛擬機器將花費較少的時間來排程執行緒,因此效能更好。

18.6.9 Object類常用的方法

方法宣告

功能介紹

void wait()

用於使得執行緒進入等待狀態,直到其它執行緒呼叫notify()或notifyAll()方法

void wait(long timeout)

用於進入等待狀態,直到其它執行緒呼叫方法或引數指定的毫秒數已經過去為止

void notify()

用於喚醒等待的單個執行緒

void notifyAll()

用於喚醒等待的所有執行緒

public class ThreadCommunicateTest implements Runnable {
    private int cnt = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                // 每當有一個執行緒進來後先大喊一聲,呼叫notify方法
                notify();
                if (cnt <= 100) {
                    System.out.println("執行緒" + Thread.currentThread().getName() + "中:cnt = " + cnt);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cnt++;
                    // 當前執行緒列印完畢一個整數後,為了防止繼續列印下一個資料,則呼叫wait方法
                    try {
                        wait(); // 當前執行緒進入阻塞狀態,自動釋放物件鎖,必須在鎖定的程式碼中呼叫
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {

        ThreadCommunicateTest tct = new ThreadCommunicateTest();
        Thread t1 = new Thread(tct);
        t1.start();

        Thread t2 = new Thread(tct);
        t2.start();
    }
}

18.6.10 執行緒池(熟悉)

(1)實現Callable介面

從Java5開始新增加建立執行緒的第三種方式為實現java.util.concurrent.Callable介面。

常用的方法如下:

方法宣告

功能介紹

V call()

計算結果並返回

(2)FutureTask類

java.util.concurrent.FutureTask類用於描述可取消的非同步計算,該類提供了Future介面的基本實現,包括啟動和取消計算、查詢計算是否完成以及檢索計算結果的方法,也可以用於獲取方法呼叫後的返回結果。

常用的方法如下:

方法宣告

功能介紹

FutureTask(Callable callable)

根據引數指定的引用來建立一個未來任務

V get()

獲取call方法計算的結果

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadCallableTest implements Callable {

    @Override
    public Object call() throws Exception {
        // 計算1 ~ 10000之間的累加和並列印返回
        int sum = 0;
        for (int i = 1; i <= 10000; i++) {
            sum +=i;
        }
        System.out.println("計算的累加和是:" + sum); // 50005000
        return sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        FutureTask ft = new FutureTask(tct);
        Thread t1 = new Thread(ft);
        t1.start();
        Object obj = null;
        try {
            obj = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒處理方法的返回值是:" + obj); // 50005000
    }
}

(3)執行緒池的由來

在伺服器程式設計模型的原理,每一個客戶端連線用一個單獨的執行緒為之服務,當與客戶端的會話結束時,執行緒也就結束了,即每來一個客戶端連線,伺服器端就要建立一個新執行緒。

如果訪問伺服器的客戶端很多,那麼伺服器要不斷地建立和銷燬執行緒,這將嚴重影響伺服器的效能。

(4)概念和原理

執行緒池的概念:首先建立一些執行緒,它們的集合稱為執行緒池,當伺服器接受到一個客戶請求後,就從執行緒池中取出一個空閒的執行緒為之服務,服務完後不關閉該執行緒,而是將該執行緒還回到執行緒池中。

線上程池的程式設計模式下,任務是提交給整個執行緒池,而不是直接交給某個執行緒,執行緒池在拿到任務後,它就在內部找有無空閒的執行緒,再把任務交給內部某個空閒的執行緒,任務是提交給整個執行緒池,一個執行緒同時只能執行一個任務,但可以同時向一個執行緒池提交多個任務。

(5)相關類和方法

從Java5開始提供了執行緒池的相關類和介面:java.util.concurrent.Executors類和 java.util.concurrent.ExecutorService介面。

其中Executors是個工具類和執行緒池的工廠類,可以建立並返回不同型別的執行緒池,常用方法如下:

方法宣告

功能介紹

static ExecutorService newCachedThreadPool()

建立一個可根據需要建立新執行緒的執行緒池

static ExecutorService newFixedThreadPool(int nThreads)

建立一個可重用固定執行緒數的執行緒池

static ExecutorService newSingleThreadExecutor()

建立一個只有一個執行緒的執行緒池

其中ExecutorService介面是真正的執行緒池介面,主要實現類是ThreadPoolExecutor,常用方法如下:

方法宣告

功能介紹

void execute(Runnable command)

執行任務和命令,通常用於執行Runnable

Future submit(Callable task)

執行任務和命令,通常用於執行Callable

void shutdown()

啟動有序關閉

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {

    public static void main(String[] args) {

        // 1.建立一個執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 2.向執行緒池中佈置任務
        executorService.submit(new ThreadCallableTest());
        // 3.關閉執行緒池
        executorService.shutdown();
    }
}