Java之多執行緒(一)
一,前言
今天總結一些關於執行緒方面的知識,說到執行緒可謂是無人不知,畢竟這東西不管是在工作開發中,還是實際生活中都時時存在著。關於執行緒方面的內容非常多,從簡單的單執行緒,多執行緒,執行緒安全以及到高併發等等,當然也包括資訊通訊。
當然這次從執行緒的基本開始,後面也會慢慢的補充執行緒的高階使用,這也算是讓自己再複習一次了(哈哈)。
以下內容包括:
- 二,執行緒介紹
- 三,執行緒的建立
- 四,執行緒安全
- 五,執行緒池
二,執行緒介紹
先來介紹幾個關於執行緒方面的概念。
2.1,並行與併發
- 併發:指兩個或多個事件在同一個時間段內發生。
- 並行:指兩個或多個事件在同一時刻發生(同時發生)。
在作業系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時執行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的,CPU在多個程式之間高速切換。
而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,並行處理的程式越多,能大大的提高電腦執行的效率。
注意:
單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發執行。同理,執行緒也是一樣的,從巨集觀角度上理解執行緒是並行執行的,但是從微觀角度上分析卻是序列執行的,即一個執行緒一個執行緒的去執行,當系統只有一個CPU時,執行緒會以某種順序執行多個執行緒,我們把這種情況稱之為執行緒排程。
2.2 ,執行緒與程序
程序:是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。
執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒 。
我們可以再電腦底部工作列,右鍵----->開啟工作管理員,可以檢視當前任務的程序:
執行緒排程:
分時排程
所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。
搶佔式排程
優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。
三,執行緒的建立
建立執行緒有兩種方式:
- 繼承
java.lang.Thread
類,重寫run方法實現執行緒建立。 - 實現
java.lang.Runnable
介面,例項化其實現類物件建立執行緒。
3.1,Thread
先來看看API文件的說明:
Thread是一個類,但同時也實現了Runnable介面。
接著使用Thread建立執行緒。
public class ThreadMain {
public static void main(String[] args) {
// 1,例項化ThreadMode物件
ThreadMode thread = new ThreadMode();
// 2,呼叫start()方法啟動執行緒
thread.start();
}
}
/**
* 繼承Thread類
*/
class ThreadMode extends Thread {
// 1,重寫父類的run方法。
@Override
public void run() {
System.out.println("使用Thread建立執行緒!");
}
}
3.2,構造方法
public Thread():
分配新的 Thread 物件。
public Thread(Runnable target):
分配一個帶有指定目標的新執行緒。
public Thread(String name):
分配一個帶有名字的新執行緒。
public Thread(Runnable target, String name):
分配一個帶有指定目標的新執行緒,並指定執行緒的名字。
3.3,Runnable
java.lang.Runnable:
Runnable 介面該由那些打算通過某一執行緒其例項的類來實現。類必須定義一個稱為run的無參方法。
實現步驟:
1,建立一個Runnable介面的實現類。
2,在實現類中重寫Runnable介面中的run方法,設定執行緒的任務。
3,建立一個Runnable實現類的物件。
4,建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件。
5,呼叫Thread類中start方法,開啟執行緒執行run方法。
public class ThreadInterface implements Runnable{
@Override
public void run() {
System.out.println("使用Runnable建立執行緒!");
}
}
// 在main方法中,建立實現類物件並開啟執行緒
ThreadInterface thread = new ThreadInterface();
new Thread(thread,"runnable").start();
說到這裡Thread類和Runnable介面都可以建立新的執行緒,那麼它們之間又有什麼區別呢?
使用Runnable介面的好處:
- 避免了單繼承的侷限性一個類只能繼承一個父類,如果建立執行緒選擇繼承Thread類,那麼就不能再繼承別的父類。而採用實現Runnable介面,則還可以再繼承,且再實現別的介面。
- 增強了程式的擴充套件性,降低了程式的耦合性(解耦)實現Runnable介面的方式,把設定執行緒任務和開啟執行緒的任務進行了分離。
3.4,匿名內部類方式建立執行緒
作用:
1,簡化程式碼
2,把子類繼承父類,重寫父類的方法,建立子類物件一步合成。
3,把實現類介面,重寫介面中的方法,建立實現類物件一步合成。
格式:
new 父類/介面(){}
Thread
new Thread(){
@Override
public void run() {
super.run();
}
}.start();
使用lambda表示式寫法(JDK8特性,後面會分享該特性):
new Thread(() -> System.out.println("Thread匿名內部類")).start();
Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Runnable匿名內部類");
}
};
// 開啟執行緒
new Thread(runnable).start();
使用lambda表示式:
Runnable runnable = () -> System.out.println("Runnable匿名內部類");
// 開啟執行緒的第一種方式
new Thread(runnable).start();
四,執行緒安全
執行緒安全通常有3種解決方式:
1,同步程式碼塊
2,同步方法
3,鎖機制(lock)
我們以賣車票為案例,用三種方式去解決車票的重複售賣,超賣情況。
4.1,同步程式碼塊
格式:
synchronized(鎖物件){
可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
}
注意:
1.通過程式碼塊中的鎖物件,可以使用任意的物件。
2.但是必須保證多個執行緒使用的鎖物件是同一個。
3.鎖物件作用: 把同步程式碼塊鎖住,只讓一個執行緒在同步程式碼塊中執行。
public class RunnableImpl implements Runnable{
//定義一個多個執行緒共享的票源
private int ticket = 100;
//建立一個鎖物件
Object obj = new Object();
//設定執行緒任務:賣票
@Override
public void run() {
//使用死迴圈,讓賣票操作重複執行
while(true){
//同步程式碼塊
synchronized (obj){
//先判斷票是否存在
if(ticket>0){
//提高安全問題出現的概率,讓程式睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,賣票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
ticket--;
}
}
}
}
}
在main方法中呼叫執行緒,並模擬三個視窗同時售票。
public class Demo01Ticket {
public static void main(String[] args) {
//建立Runnable介面的實現類物件
RunnableImpl run = new RunnableImpl();
//建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//呼叫start方法開啟多執行緒
t0.start();
t1.start();
t2.start();
}
}
4.2,同步方法
使用步驟:
1.把訪問了共享資料的程式碼抽取出來,放到一個方法中
2.在方法上新增synchronized修飾符
格式:定義方法的格式
修飾符 synchronized 返回值型別 方法名(引數列表){
可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
}
public class RunnableImpl implements Runnable{
//定義一個多個執行緒共享的票源
private static int ticket = 100;
//設定執行緒任務:賣票
@Override
public void run() {
System.out.println("this:"+this);
//使用死迴圈,讓賣票操作重複執行
while(true){
payTicketStatic();
}
}
/*
靜態的同步方法
鎖物件應該是誰?
不能是this
this是建立物件之後產生的,靜態方法優先於物件
靜態方法的鎖物件是本類的class屬性-->class檔案物件(反射)
*/
public static /*synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class){
//先判斷票是否存在
if(ticket>0){
//提高安全問題出現的概率,讓程式睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,賣票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
ticket--;
}
}
}
/*
定義一個同步方法
同步方法也會把方法內部的程式碼鎖住
只讓一個執行緒執行
同步方法的鎖物件是誰?
就是實現類物件 new RunnableImpl()
也是就是this
*/
public /*synchronized*/ void payTicket(){
synchronized (this){
//先判斷票是否存在
if(ticket>0){
//提高安全問題出現的概率,讓程式睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,賣票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
ticket--;
}
}
}
}
在main函式中呼叫該執行緒,其程式碼與上述一樣。
4.3,同步鎖(Lock)
java.util.concurrent.locks.Lock
介面
Lock 實現提供了比使用 synchronized
方法和語句可獲得的更廣泛的鎖定操作。
Lock介面中的方法:
void lock()獲取鎖。
void unlock() 釋放鎖。
使用步驟:
1.在成員位置建立一個ReentrantLock物件。
2.在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖。
3.在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖 。
請看如下API說明:
public class RunnableImpl implements Runnable{
//定義一個多個執行緒共享的票源
private int ticket = 100;
//1.在成員位置建立一個ReentrantLock物件
Lock l = new ReentrantLock();
//設定執行緒任務:賣票
@Override
public void run() {
//使用死迴圈,讓賣票操作重複執行
while(true){
//2.在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖
l.lock();
//先判斷票是否存在
if(ticket>0){
try {
//票存在,賣票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖
l.unlock();//無論程式是否異常,都會把鎖釋放
}
}
}
}
}
五,執行緒池
如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒是需要時間的。
在JDK5之前,對於執行緒池的使用是需要程式設計師用集合來自己進行建立。在JDK5之後就不再需要手動去建立,JDK已經幫我們封裝好了。
5.1,執行緒池概念
- 執行緒池:其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。
用一張簡單的圖來理解下執行緒池工作的原理。
合理利用執行緒池能夠帶來三個好處:
- 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
- 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,可能最後宕機)。
5.2,使用方式
Java裡面執行緒池的頂級介面是java.util.concurrent.Executor
,但是嚴格意義上講Executor
並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是java.util.concurrent.ExecutorService
。
要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,很有可能配置的執行緒池不是較優的,因此在java.util.concurrent.Executors
執行緒工廠類裡面提供了一些靜態工廠,生成一些常用的執行緒池。官方建議使用Executors工程類來建立執行緒池物件。
Executors類中有個建立執行緒池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回執行緒池物件。(建立的是有界執行緒池,也就是池中的執行緒個數可以指定最大數量)
獲取到了一個執行緒池ExecutorService 物件,那麼怎麼使用呢,在這裡定義了一個使用執行緒池物件的方法如下:
public Future<?> submit(Runnable task)
:獲取執行緒池中的某一個執行緒物件,並執行Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用。
使用執行緒池中執行緒物件的步驟:
- 建立執行緒池物件。
- 建立Runnable介面子類物件。(task)
- 提交Runnable介面子類物件。(take task)
- 關閉執行緒池(一般不做,因為再次使用的時候執行緒池中就沒有執行緒了)。
public class MyRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒: " + Thread.currentThread().getName());
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// 建立執行緒池物件
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個執行緒物件
// 建立Runnable例項物件
MyRunnable r = new MyRunnable();
// 從執行緒池中獲取執行緒物件,然後呼叫MyRunnable中的run()
service.submit(r);
// 再獲取個執行緒物件,呼叫MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法呼叫結束後,程式並不終止,是因為執行緒池控制了執行緒的關閉。
// 將使用完的執行緒又歸還到了執行緒池中
}
}
六,sleep()與wait()
1,所屬分類不同,sleep屬於Thread類中,而wait屬於Object類中。
2,鎖控制不同,sleep不會釋放鎖,而wait會釋放鎖且不會影響其他執行緒進入同步程式碼塊或同步方法中。也就是說sleep會佔用資源,wait不會佔用資源。
3,sleep可以在任意地方使用,而wait需在同步程式碼塊或者同步方法中使用。
七,volatile與synchronized
1,volatile效能比synchronized要好,因為volatile是執行緒同步的輕量級實現。
2,volatile只能修飾變數,synchronized可以修飾方法以及程式碼塊。
3,多執行緒訪問volatile不會發生阻塞,synchronized會出現阻塞。
4,volatile能保證資料的可見性,但不能保證原子性。synchronized可以保證原子性,也可以間接的保證可見性。
5,volatile解決的是變數在多執行緒之間的可見性。synchronized解決的是多執行緒之間訪問資源的同步性。
6,volatile防止指令重排。
八,總結
似乎覺得本人的每一篇部落格的篇幅都好長,可能是因為都是一些很基礎的知識點吧,所以就會涉及到很多方方面面,寫著寫著就很多了。不過這樣記下來時間久了還可以再回來看看,多少也算有點印象(哈哈)。
如果你閱讀到此,很感謝您的耐心。以上總結的內容,如有不適之處,歡迎留言指正。
感謝閱讀