關于多執行緒的初步學習(上)
執行緒的概述
行程:在記憶體中正在執行的程式,一個應用程式如果想被執行,需要跑在記憶體中,
執行緒:是行程的一個執行單元,用來負責行程中程式(代碼)的執行,在行程中可以有一條執行緒,也可以有多條執行緒,如果只有一條執行緒,稱之為單執行緒程式,如果有多條執行緒,稱之為多執行緒程式,一個行程至少需要一條執行緒,
java中執行緒使用搶占式調度:執行緒具有優先級,優先級高的搶占到執行緒CPU資源概率會更大,如果執行緒的優先級相同,那么會隨機選擇一個執行緒執行,優先級分為1~10,理論上數字越大,優先級越高,但實際上相鄰的優先級之間差距非常不明顯,可以使用**setPriority()**來設定優先級,
創建執行緒的某些方法:
-
繼承Thread類
定義一個類繼承Thread,
重寫run方法,
創建子類物件,就是創建執行緒物件,
呼叫start方法,開啟執行緒并讓執行緒執行,同時還會告訴jvm去呼叫run方法 -
定義類實作Runnable介面,
覆寫介面中的run方法,
創建Thread類的物件 將Runnable介面的子類,
物件作為引數傳遞給Thread類的建構式,
呼叫Thread類的start方法開啟執行緒,
既然提到執行緒,那肯定會提到執行緒安全問題,那什么是執行緒安全呢?如果不考慮執行緒安全,那會出現什么情況呢?我們用一個售票的例子來進行模擬:
// 模擬票數
public class Ticket implements Runnable{
int ticket = 100;
@Override
public void run() {
// TODO Auto-generated method stub
while (true){
// 讓當前執行緒休眠 單位是毫秒
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 說明票賣光了
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在賣" + ticket--);
}
}
}
測驗代碼:
public static void main(String[] args) {
// 創建票物件
Ticket ticket = new Ticket();
// 通過Thread來模擬視窗
Thread t1 = new Thread(ticket,"視窗1");
Thread t2 = new Thread(ticket,"視窗2");
Thread t3 = new Thread(ticket,"視窗3");
// 開啟執行緒進行賣票
t1.start();
t2.start();
t3.start();
}
如果不考慮執行緒安全問題的話,那么會出現以下情況:
- 出現了重復的票
- 出現了錯誤的票 0、-1
原因是當某個執行緒進入某句代碼后,可能執行了一部分資源就被其他執行緒給搶了過去,從而導致該執行緒的資料進行了一般,可能剛輸出完還沒寫入記憶體,這時被另一條執行緒搶走,此時資料還未進行修改,所以就出現了重復資料,而0,-1這種就是數量為1時剛通過上面的判斷,但還未進行輸出,這時資源被其他執行緒搶走,從而導致在數量為1時三條執行緒都通過了判斷,再依次進行輸出和自減操作,
執行緒安全:
如果有多個執行緒在同時運行,而這些執行緒可能會同時運行這段代碼,程式每次運行結果和單執行緒運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的,
而售票案例明顯出了問題,就是存在執行緒安全隱患,
解決思路:保證核心代碼同一時刻只能有一條執行緒在執行,
那么如和保證執行緒安全呢?一般我們使用這幾種方法
- 同步代碼塊
格式:synchronized (鎖物件) {
可能會產生執行緒安全問題的代碼
}
同步代碼塊中的鎖物件可以是任意的物件;但多個執行緒時,要使用同一個鎖物件才能夠保證執行緒安全,
方法區的內容是被執行緒共享的,所以"abc" Math.class 都可以當做同步代碼塊的鎖資源 ,
同步代碼塊會影響多執行緒的效率,所以只加核心可能出現隱患的代碼,
- 同步方法
我們可以使用synchronized 修飾方法,可以保證方法中同一時刻只能有一條執行緒在執行,
同步方法的鎖資源是this,
- 靜態同步方法:
被synchronized和static修飾的方法,可以保證方法中同一時刻只能有一條執行緒在 執行,
靜態同步方法的鎖資源是 類名.class,
通過上面幾種方法,我們可以修改一下我們的代碼
// 模擬票數
public class Ticket implements Runnable{
int ticket = 100;
// 當做同步代碼塊的鎖資源
Object obj = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
while (true){
// 讓當前執行緒休眠 單位是毫秒
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// // 同步代碼塊
synchronized (obj) {
// 說明票賣光了
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在賣" + ticket--);
}
}
}
}
總結:
同步:只允許一個執行緒執行,此時執行緒是安全的;
異步:允許多執行緒同時執行,此時就有可能出現執行緒安全隱患,
執行緒安全隱患出現的條件:
- 存在多個執行緒,
- 擁有共享的資源,
- 對資源進行非原子性操作,
解決執行緒安全的方法:
1.同步代碼塊;
鎖資源是任何共享的物件,
2.同步方法;
鎖資源是this,
3.靜態同步方法;
鎖資源是類名.class,
補充:
什么是原子操作呢”?
可以理解為類似于資料庫的事務,畢竟事務也有原子性,這個(組)操作在執行時不會被其他原因打斷,要么執行成功,要么全部失敗,不存在一半成功一半失敗的可能,
那么原子操作都有哪些呢?
(1)除long和double之外的基本型別的賦值操作
(2)所有參考reference的賦值操作
(3)java.concurrent.Atomic.* 包中所有類的一切操作
注:count++不是原子操作,因為它可以拆分為三個原子操作,
那為什么long和double的賦值不屬于原子操作呢?
在網上看到某篇文章,大致意思如下:
1.對一個沒有使用volatile修飾的long或double型別的賦值會被拆分成兩次寫,每次寫該型別的32-bit資料,再使用volatile修飾后的long和double型別的讀寫操作是原子性的,
2.對其參考型別(Long/Double)的讀寫操作是屬于原子操作,盡管他們的實作可能被分為兩次32-bit或者一個64-bit,
死鎖deadlock:
所謂死鎖,是指多個行程在運行程序中因爭奪資源而造成的一種僵局,當行程處于這種僵持狀態時,若無外力作用,它們都將無法再向前推進,
出現死鎖的原因是有鎖的嵌套,類似于一個執行緒A套著B,另執行緒一個B套著A,此時兩個執行緒都在進行,A和B都被鎖住,等著另一方釋放資源,結果誰也不釋放,造成死鎖,
活鎖
除死鎖外還有一個更恐怖的bug——活鎖,
活鎖指的是任務或者執行者沒有被阻塞,由于某些條件沒有滿足,導致一直重復嘗試—失敗—嘗試—失敗的程序,處于活鎖的物體是在不斷的改變狀態,活鎖有可能自行解開,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275412.html
標籤:其他
