關於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(); } }