理解多執行緒 (篇一)
執行緒:通常每一個任務稱為一個執行緒(Thread),他是執行緒控制的簡稱,執行緒是CPU最小的執行單元,也可以理解為是一個程式裡面不同的執行路徑。每個執行緒都有獨立的執行棧和程式計數器(PC)。Java虛擬機器一般預設有兩個執行緒,一個是主執行緒main,另一個是垃圾回收執行緒。
既然CPU同一時間只能處理一個執行緒,那為什麼我們平時在電腦上一邊聽歌一邊看電影呢,那是因為CPU等概率的在各個執行緒中相互切換的頻率特別快,快到我們感官無法感知。執行緒排程的細節依賴於作業系統的服務,
執行緒的執行狀態
執行緒的三種建立方式:
One:
- 寫一個類繼承Thread類,實現run()方法,然後使用該類物件的start方法來啟動執行緒。(一般很少用這種方式,除非Thread類寫的方法不滿足你的需求)
Two:
- 寫一個類來實現Runnable介面,實現run()方法,建立該類物件並作為引數傳遞給Thread類,並呼叫start方法開始啟動執行緒。
Three:
- 建立一個匿名內部類物件,呼叫start方法來開啟執行緒。
例:
package net.csdn.qf.thread; /** * @author 北冥有熊 * 2018年11月6日 */ public class ThreadTest { public static void main(String[] args) { //第一種方式啟動 new Demo().start(); //建立Demo物件並呼叫start方法啟動執行緒 //第二種方式啟動 Demo1 demo1 = new Demo1(); //建立Demo1物件 new Thread(demo1).start(); //將物件作為引數傳遞給Thread,並呼叫start方法開始啟動執行緒 //第三種方式啟動 new Thread(new Runnable() { //直接使用匿名內部類物件來作為構造引數來建立執行緒 @Override public void run() { // TODO Auto-generated method stub for (int i=1; i<=50; i++) { System.out.println("第3種執行緒執行--->"+i+"(3)"); } } }).start(); } } //第一種類 class Demo extends Thread{ @Override public void run() { // TODO Auto-generated method stub for (int i=1; i<=50; i++) { System.out.println("第1種執行緒執行--->"+i+"(1)"); } } } //第二種類 class Demo1 implements Runnable{ @Override public void run() { // TODO Auto-generated method stub for (int i=1; i<=50; i++) { System.out.println("第2種執行緒執行--->"+i+"(2)"); } } }
獲取執行緒物件及名稱
currentThread:獲取當前執行緒物件
currentThread.getName: 獲取當前執行緒的名字
currentThread.getId:獲取當前執行緒的Id
下面以買票為例:
package net.csdn.qf.thread; /** * @author 北冥有熊 * 2018年11月6日 */ public class Test01 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(ticket,"小明").start(); new Thread(ticket,"郭靖").start(); new Thread(ticket,"黃蓉").start(); new Thread(ticket,"楊康").start(); } } class Ticket implements Runnable{ int num = 100; @Override public void run() { while(num>=1) { if(num>=1) { try { Thread.sleep(50);//睡眠500ms } catch (InterruptedException e) { e.printStackTrace(); } Thread a = Thread.currentThread();//建立當前執行緒物件 System.out.println("當前執行緒物件為: "+a.getName()+" 賣出的票號是--->"+num);//輸出當前執行緒的名字 num--; } } } }
結果是:
當前執行緒物件為: 小明 賣出的票號是--->20
當前執行緒物件為: 郭靖 賣出的票號是--->20 當前執行緒物件為: 郭靖 賣出的票號是--->8
當前執行緒物件為: 楊康 賣出的票號是--->20 當前執行緒物件為: 小明 賣出的票號是--->8
當前執行緒物件為: 黃蓉 賣出的票號是--->20 當前執行緒物件為: 黃蓉 賣出的票號是--->8
當前執行緒物件為: 郭靖 賣出的票號是--->16 當前執行緒物件為: 小明 賣出的票號是--->4
當前執行緒物件為: 黃蓉 賣出的票號是--->16 當前執行緒物件為: 楊康 賣出的票號是--->4
當前執行緒物件為: 小明 賣出的票號是--->16 當前執行緒物件為: 小明 賣出的票號是--->0
當前執行緒物件為: 楊康 賣出的票號是--->16 當前執行緒物件為: 楊康 賣出的票號是--->0
當前執行緒物件為: 郭靖 賣出的票號是--->12 當前執行緒物件為: 郭靖 賣出的票號是--->0
當前執行緒物件為: 楊康 賣出的票號是--->12 當前執行緒物件為: 郭靖 賣出的票號是--->-1
當前執行緒物件為: 黃蓉 賣出的票號是--->12
當前執行緒物件為: 小明 賣出的票號是--->12
很明顯,不同執行緒物件賣出了重複的票甚至還出現負數票,這在日常的買票系統中是絕對不能允許的。
出現復票和負票的原因:
復值原因:當執行到輸出 當前執行緒執行物件的時候,一個執行緒物件(黃蓉)執行完輸出語句後,還未執行num--語句時,第二個執行緒物件(郭靖)緊接著也執行到了該輸出語句,這就導致了出現復票的原因。
負值原因:當num==0時,一個執行緒物件(黃蓉)執行完輸出語句後,再執行完num-- 語句,第二個執行緒物件(郭靖)緊接著也執行到了輸出語句,這時先執行的執行緒物件(黃蓉)因為已經執行了num自減語句,此時num已經變為-1,緊跟第一個執行緒物件其後的第二個執行緒物件輸出的是 num== -1,這就是出現負值的原因。
多執行緒安全問題
當多個執行緒同時操作同一個共享資料時,操作資料包括判斷,修改,同一個執行緒在沒有處理完資料的時候別的執行緒參與了資料運算,導致資料發生異常。
synchronized:
在Java中,synchronized關鍵字是用來控制執行緒同步的,就是在多執行緒的環境下,控制synchronized程式碼段不被多個執行緒同時執行。synchronized既可以加在一段程式碼上,也可以加在方法上。
同步程式碼塊:將一次只希望一個執行緒物件處理的程式碼塊寫在synchronized(this){ }的方法體內 ,此處this表示當前物件,意思是隻允許當前this物件執行方法體內的程式碼塊,表示一個通行證(加鎖)。對於static的synchronized方法,因為沒有this物件,因此鎖的就是這個類的Class物件,如:synchronized(XXX.class){ 程式碼塊 }。
同步方法:使用synchronized修飾方法,在呼叫該方法前,需要獲得內建鎖(java每個物件都有一個內建鎖),否則就處於阻塞狀態。例如:public synchronized void save(){//內容}。
下面以多執行緒物件在銀行取款為例,解釋說明同步程式碼塊與同步方法。
package net.csdn.qf.test;
/**
* @author 北冥有熊
* 2018年11月7日
*/
public class Test {
public static void main(String[] args) {
Bank bank = new Bank();//銀行物件
Account account = new Account(bank); //賬戶物件
new Thread(account,"郭靖").start();
new Thread(account,"黃蓉").start();
new Thread(account,"楊康").start();
}
}
//銀行存款
class Bank{
int money = 1000;
}
//賬戶
class Account implements Runnable{
static Bank bank = null;
public Account(Bank bank) {
Account.bank = bank;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(bank.money>=100) {
synchronized (this) { //同步程式碼塊,加鎖只允許當前this訪問。
//此時,所有被this鎖住的同步程式碼塊以及同步方法都被同步鎖住。
if(bank.money>=100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String name = Thread.currentThread().getName(); //建立當前執行緒物件
System.out.println("run當前執行緒名:"+name+" 餘額--->"+bank.money);
bank.money-=100; //每次取100
}
}
tes();//呼叫靜態同步方法tes(),因為上述同步程式碼塊鎖住的是this,
//而靜態方法沒有this,所以tes方法並沒有被鎖住,任然出現執行緒安全問題。
}
}
public static synchronized void tes() { //靜態同步方法,因為沒有this,所以只能被Account.class鎖住。
if(bank.money>=100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String name = Thread.currentThread().getName(); //建立當前執行緒物件
System.out.println("tes當前執行緒名:"+name+" 餘額--->"+bank.money);
bank.money-=100; //每次取100
}
}
}
因為synchronized(this)沒有鎖住tes()方法,故結果仍有重複值,當將this換為Account.class鎖住後,執行緒安全問題得到解決。
用this鎖住 用Account.class鎖住
run當前執行緒名:郭靖 餘額--->1000 run當前執行緒名:郭靖 餘額--->1000
tes當前執行緒名:郭靖 餘額--->900 tes當前執行緒名:郭靖 餘額--->900
run當前執行緒名:黃蓉 餘額--->900 run當前執行緒名:郭靖 餘額--->800
tes當前執行緒名:黃蓉 餘額--->700 run當前執行緒名:黃蓉 餘額--->700
run當前執行緒名:楊康 餘額--->700 tes當前執行緒名:黃蓉 餘額--->600
run當前執行緒名:黃蓉 餘額--->500 run當前執行緒名:楊康 餘額--->500
tes當前執行緒名:楊康 餘額--->500 tes當前執行緒名:楊康 餘額--->400
run當前執行緒名:郭靖 餘額--->300 run當前執行緒名:楊康 餘額--->300
tes當前執行緒名:黃蓉 餘額--->300 tes當前執行緒名:楊康 餘額--->200
run當前執行緒名:楊康 餘額--->100 run當前執行緒名:黃蓉 餘額--->100
tes當前執行緒名:郭靖 餘額--->100
擴充套件(面試題):
大家都知道,對於單例餓漢模式來說,當程式開始執行時,常量池中已經建立了一個不可變物件,因此不用擔心多執行緒安全問題。但對於懶漢單例模式而言,就沒有那麼好了。。。
例:
package net.csdn.qf.test;
/**
* @author 北冥有熊
* 2018年11月7日
*/
public class Test01 {
public static void main(String[] args) {
int num = 0;
while(num<=50) {
new Thread(new Runnable() {
@Override
public void run() {
//EHan.getEHan(); //餓漢模式,因為物件在常量池中共享,所以共享
//不會出現多執行緒安全問題
LHan.getLHan(); //懶漢模式,出現多執行緒安全問題
}
}).start();
num++;
}
}
}
//餓漢單例
class EHan{
private static final EHan E_Han = new EHan();
private EHan() {
System.out.println("建立了餓漢物件");
}
public static EHan getEHan() {
return E_Han;
}
}
//懶漢單例
class LHan{
private static LHan lanHan = null;
private LHan() {
System.out.println("建立了懶漢物件");
}
public static LHan getLHan() {
if(lanHan==null) {
lanHan = new LHan();
}
return lanHan;
}
}
結果:
餓漢模式 懶漢模式
建立了餓漢物件 建立了懶漢物件
建立了懶漢物件
建立了懶漢物件
建立了懶漢物件
原因還是一樣,在懶漢模式下進入if判斷語句時,當建立物件語句還沒有執行完,其餘在外面等的三個也已經判斷了if(nanhan==null),因此建立了多個物件,解決辦法就是加上同步程式碼塊。面試題的重點來了.......
加上同步程式碼塊後,雖然多執行緒安全問題解決了,但也緊接著出現另一個問題,執行緒阻塞。當執行緒達到一定數量的時候,第一個想成通過同步程式碼塊建立物件後,在其後面的執行緒都會判斷兩次(第一次:判斷鎖;第一次:判斷物件是否為空),執行緒數量多的時候就會出現阻塞。
解決辦法:雙重判斷來減少比較次數。
class LHan{
private static LHan lanHan = null;
private LHan() {
System.out.println("建立了懶漢物件");
}
public static LHan getLHan() {
if(lanHan==null) { //雙重判斷,解決執行緒阻塞
synchronized (LHan.class) { //加鎖
if(lanHan==null) {
lanHan = new LHan();
}
}
}
return lanHan;
}
}
儘管在平時處理多執行緒安全問題中基本都用餓漢單例模式,懶漢單例模式被忽略,但這卻是一道檢查基本功的面試題,要理解哦。到時候面試官問你時,可別說我沒有說哦!
更多內容見篇二!