多執行緒
- 一、行程、執行緒
- 1.1 行程
- 1.2 執行緒
- 1.3 java程式的行程和執行緒
- 1.4 主執行緒、子執行緒
- 二、多執行緒
- 2.1 單核cup可以做到多執行緒并發嗎?
- 2.2 實作多執行緒方式
- 2.2.1 子類繼承父類(Thread)
- 2.2.2 start()方法
- 2.2.3 實作Runnable介面
- 2.2.4 采用匿名內部類
- 2.2.5 實作callable介面
- 2.3 獲取和修改執行緒的名字
- 三、 執行緒生命周期
- 3.1 新建狀態
- 3.2 就緒狀態
- 3.3 運行狀態
- 3.4 死亡狀態
- 3.5 阻塞狀態
- 3.5.1 執行緒sleep(休眠方法)
- 3.5.2 執行緒interrupt("叫醒執行緒方法")
- 3.5.3 執行緒stop(強行中斷執行緒)
- 四、執行緒的調度
- 4.1 常見的調度模型
- 4.2 java中的執行緒調度方法
- 五、多執行緒安全(重點)
- 5.1 什么時候會出現多執行緒并發的安全問題
- 5.2 解決方法(執行緒同步機制)
- 5.3 變數的執行緒安全問題
- 5.4 死鎖
- 六、 守護執行緒
- 6.1 守護執行緒的實作
- 6.2 定時器
- 七、notify和wait方法
- 7.1 wait方法
- 7.2 notify方法
- 7.3 兩種方法的使用
一、行程、執行緒
java語言之所以有多執行緒,目的是提高程式的運行效率,
1.1 行程
行程是一個應用程式(一個行程是一個軟體),
1.2 執行緒
執行緒是一個行程中的執行場景、執行單元,一個行程可以啟動多個執行緒
1.3 java程式的行程和執行緒
對于java程式來說,當在DOS命令視窗中輸入:
java HelloWorld 回車之后,會先啟動JVM,而JVM就是一個行程,
JVM再啟動一個主執行緒呼叫主方法(main),同時在啟動一個垃圾回收的執行緒負責看護,回收垃圾,
所以,目前java程式至少有2個執行緒并發,一個是執行main方法的主執行緒,一個是垃圾回收執行緒,
兩個行程記憶體獨立不會共享,同一個行程的兩個執行緒堆記憶體和方法區共享,但是堆疊記憶體獨立,一個執行緒一個堆疊,
假設有10個執行緒就有十個堆疊,每個堆疊直接互不干擾,各自執行,這就是多執行緒并發,
1.4 主執行緒、子執行緒
主執行緒結束了,子執行緒還會執行嗎?
Thread t1=new Thread(new Runnable() {
public void run() {
for (int i = 0; i <= 100; i++) {
if(i==100) {
System.out.println("子執行緒執行結束了!");
}
}
}
});
t1.start();
System.out.println("主執行緒執行結束了!");
運行結果:
主執行緒執行結束了!
子執行緒執行結束了!

通過測驗可以看出主執行緒結束了,子執行緒仍然還在運行
有沒有函式可以讓主執行緒等子執行緒結束了才結束運行主執行緒
有的,
通過在主執行緒的任務中用子執行緒呼叫join()就可,
join解釋成:等待執行緒死亡,
public static void main(String[] args) throws InterruptedException {
test t2=new test();
t2.start();
t2.join();
System.out.println("主執行緒執行");
}
}
class test extends Thread{
public void run() {
System.out.println("子執行緒執行");
}
}
輸出結果:
子執行緒執行
主執行緒執行
二、多執行緒
2.1 單核cup可以做到多執行緒并發嗎?
什么是真正的多執行緒并發?對于多核cpu電腦真正的多執行緒并發肯定沒有問題,
t1執行緒執行t1的,t2執行緒執行t2的,t1不會影響t2的,t2也不會影響t1的,這就做真正的多執行緒并發,
單核的cup只有一個‘大腦’,不能做到真正的多執行緒并發,但是可以給人一種多執行緒并發的感覺,
對于單核的cup來說,在某個時間點上實際只能處理一件事情,由于cup速度極快,多個執行緒頻繁切換,給人造成多
執行緒并發的錯覺,
舉個例子:
網上最近很火的小書,通過大拇指的松動給人一種畫面在動的錯覺,
還有很著名的”膝跳反應原理“,拿小錘在你膝蓋上敲,到感到痛覺,對于人的反應來說已經很快了,覺得就是同步的,其實不是,它會有個反應時間,這個反應時間對于我們人來說感覺不到,但是對于計算機來說,可以進行數億次運算了,人類的大腦就好比單核計算機的cpu,
2.2 實作多執行緒方式
2.2.1 子類繼承父類(Thread)
直接通過子類去繼承父類(Thread),重寫里面的run()方法
public static void main(String[] args) {
//main方法在主執行緒中,在主堆疊運行
//創建一個執行緒的物件
MyThread myThread=new MyThread();
//啟動執行緒
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主執行緒----->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
// 撰寫程式,運行在分支執行緒(堆疊)中
for (int i = 0; i < 10; i++) {
System.out.println("分支執行緒---->"+i);
}
}
}
2.2.2 start()方法
start()方法的作用是:在JVM中開辟一塊新的堆疊空間,開啟后start()方法瞬間結束,
只要空間開出來,執行緒就啟動成功了,分支執行緒自動呼叫run()方法執行程式,并且start()方法會在分支堆疊的最底部,和主執行緒的main方法差不多,

下面代碼有什么區別?
myThread.run();
myThread.start();
直接呼叫run方法,分支堆疊并沒有開辟出來,所以還是單執行緒,
而呼叫start方法,分支堆疊開辟出來,啟動了多執行緒,
2.2.3 實作Runnable介面
Runnable并不是一個執行緒類,而是一個可運行的類,在主執行緒中創建一個可運行的物件,將可運行的物件封裝成一個執行緒物件,
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主執行緒----->"+i);
}
}
}
class MyRunnable implements Runnable{
public void run() {
// 撰寫程式,運行在分支執行緒(堆疊)中
for (int i = 0; i < 10; i++) {
System.out.println("分支執行緒---->"+i);
}
}
2.2.4 采用匿名內部類
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
public void run() {
System.out.println("支執行緒運行");
}
});
t.start();
System.out.println("主執行緒執行");
}
2.2.5 實作callable介面
眾所周知,前面兩種執行緒不會有回傳值,還有另一種執行緒的實作方法可以有回傳值
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 創建未來任務物件
FutureTask <Integer>future=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int a=10;
int b=20;
return a+b;
}
});
Thread t1=new Thread(future);
t1.start();
System.out.println(future.get());
}
小結:這種執行緒實作的方式可以獲取到執行緒結束后回傳的值,call方法類似于run方法,不同的是:call方法會有回傳值,
這讓我想起來前一段時間的一道考試題,這道題我就卡在了不能獲取執行緒的回傳結果,學習到這,我重新想到那題的解決辦法:
問題是:創建2個執行緒,一個執行緒求100內的偶數,一個執行緒求100內余數,并求和,
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> future=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int num=0;
for (int i = 1; i <=100; i++) {
if(i%2==0) {
num+=i;
}
}
return num;
}
});
FutureTask<Integer> future2=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int num2=0;
for (int i = 1; i <=100; i++) {
if(i%2!=0) {
num2+=i;
}
}
return num2;
}
});
Thread t1=new Thread(future);
Thread t2=new Thread(future2);
t1.start();
t2.start();
System.out.println(future.get()+future2.get());
小結:這種方法優點是:可以獲取到執行緒的回傳值,缺點是,效率低,在獲取執行緒回傳值的時候,當前執行緒會阻塞,
2.3 獲取和修改執行緒的名字
執行緒有默認的名字 ---->Thread-0
可以通過setName方法進行修改執行緒名字,
獲取到當前執行緒物件
Thread t=Thread.currentThread();
回傳值t就是當前執行緒,如果出現在主執行緒中當前執行緒就是主執行緒,
三、 執行緒生命周期

3.1 新建狀態
新建狀態是新new出來的執行緒物件,
3.2 就緒狀態
就緒狀態又叫可運行狀態,表示當前的執行緒具有搶奪cpu時間片的權力(CPU時間片就是執行權),
當一個執行緒搶奪到cpu時間片之后,就會開始執行run方法,run方法執行代表著執行緒進入運行狀態,
3.3 運行狀態
run方法開始執行標志著這個執行緒進入運行狀態,當之前占有的cpu時間片用完之后,會重新回到就緒狀態繼續搶奪cpu時間片,當再次搶到cpu時間片之后,會重新進入run方法接著上一次的代碼繼續往下執行,
3.4 死亡狀態
當run方法執行完,執行緒死亡,
3.5 阻塞狀態
當一個執行緒進入到阻塞事件,例如接受用戶的鍵盤輸入,或者sleep方法等,此時執行緒會進入阻塞狀態,阻塞狀態的執行緒會放棄之前占有的
cpu時間片,重新進入到就緒狀態繼續搶cpu時間片,
3.5.1 執行緒sleep(休眠方法)
static void sleep(long millis);
sleep是一個靜態的方法,引數是毫秒,作用是讓當前執行緒進入休眠,進入’阻塞狀態‘,放棄占有的cpu時間片,讓給其他執行緒使用,
public static void main(String[] args) {
System.out.println("主執行緒執行");
try {
Thread.sleep(5000);
} catch (Exception e) {
e.getStackTrace();
}
System.out.println("執行緒休眠5秒后輸出了");
}

5秒后:

面試題:判斷下面代碼運行效果
public static void main(String[] args) {
Thread t=new MyThread2();
t.setName("t");
t.start();
try {
//t執行緒會休眠嗎??
t.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主執行緒");
}
}
class MyThread2 extends Thread{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("當前執行緒"+Thread.currentThread().getName()+"\t"+i);
}
}
}
分析:t.sleep()出現在主執行緒中,雖然它是由執行緒物件點出來的,但是它是靜態方法和參考沒有任何關系,出現在哪個地方,就會讓哪個執行緒進入休眠,所有說這個程式會先執行分執行緒,5秒后輸出”主執行緒“,
3.5.2 執行緒interrupt(“叫醒執行緒方法”)
interrupt方法叫”干擾“,會中斷執行緒的睡眠,靠的是java的例外處理機制,
public static void main(String[] args) {
Thread t=new MyThread2();
//分支執行緒開啟
t.start();
try {
Thread.sleep(2000);//主執行緒休息2秒
} catch (Exception e) {
e.printStackTrace();
}
t.interrupt();//讓分支執行緒型“醒過來”
}
}
class MyThread2 extends Thread{
public void run() {
try {
Thread.sleep(20000000);//執行緒休眠很長時間
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("分支執行緒");
}
}

理解:當開始運行run方法的時候,執行緒會進入長時間休眠狀態,通過執行interrupt方法會讓分支執行緒出例外,直接執行catch陳述句,
3.5.3 執行緒stop(強行中斷執行緒)
public static void main(String[] args) {
Thread t=new MyThread2();
t.start();
//主執行緒休息5秒鐘直接后干死分支執行緒
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
t.stop();//強行中斷執行緒
}
}
class MyThread2 extends Thread{
public void run() {
//這個程式會運行10秒鐘
for (int j = 0; j <10; j++) {
System.out.println(Thread.currentThread().getName()+j);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

stop的缺點:容易丟失資料,執行緒沒有保存的資料會丟失,所以過時了,
四、執行緒的調度
4.1 常見的調度模型
搶占式:哪個執行緒的優先級比較高,搶到的cpu時間片的概率就多一些,java采用的就是搶占式調度模型,
均分式:平均分配cpu時間片,每個執行緒占有的cpu時間片時間長度一樣,平均分配
4.2 java中的執行緒調度方法
實體方法:
void setpriority(int newPriority)//設定執行緒優先級
int getPriority()//獲取執行緒優先級
靜態方法:
static void yield()//暫停當前正在執行的執行緒,并執行其他執行緒
yield方法不是阻塞,會讓當前執行緒”運行狀態“回到”就緒狀態“,
最低優先級是1,默認是5,最高是10.
五、多執行緒安全(重點)
5.1 什么時候會出現多執行緒并發的安全問題
需要滿足三個條件
1.多執行緒并發環境下,
2.有共享資料,
3.共享的資料有修改行為,
5.2 解決方法(執行緒同步機制)
執行緒排隊執行(不能并發),用排隊執行解決執行緒安全問題,會犧牲一部分效率,
這種機制被成為:執行緒同步機制,
異步編程模型:執行緒t1和t2,各自執行,就做異步編程模型,就是多執行緒并發,
同步執行緒模型:執行緒t1執行的時候,必須等待執行緒2執行結束,兩個執行緒之間出現了等待關系,這就是同步執行緒模型,
怎么實作同步機制呢,synchronized代碼塊
每一個堆中的物件都有一把鎖,這把鎖只是一個標記,
假設t1和t2執行緒并發,執行緒1先執行了,遇到了synchronized,這個時候自動找后面執行緒共享物件的物件鎖,找到之后并占有一把鎖,然后執行
同步代碼塊中的程式,在執行程序中會、一直占有這把鎖,直到同步代碼塊結束,這把鎖才會釋放,
總而言之,一定時間只有一個執行緒會執行程式,下個執行緒在同步代碼塊外等待,
注意事項:需要同步執行緒一定要有個共享物件,
5.3 變數的執行緒安全問題
在java中有三大變數:
實體變數:在堆中
靜態變數:在方法區
區域變數:在堆疊中
在這三者中只有區域變數永遠不會有執行緒安全問題,原因在于區域變數在堆疊中,永遠都不會共享,
解決方法:
1.使用區域變數代替實體和靜態變數
2.如果用實體變數,多new物件,
3.迫不得已用synchronized,用執行緒同步機制,
5.4 死鎖
就是多個執行緒在運行狀態中因爭奪資源造成的一種僵局,

六、 守護執行緒
6.1 守護執行緒的實作
在Java中,有兩大類執行緒:
第一種是用戶執行緒,第二種是守護執行緒(也稱作后臺執行緒),
守護執行緒具有代表是java中的垃圾回收執行緒,
主執行緒是一個用戶執行緒,
守護執行緒的特點:
1.一般守護執行緒都是死回圈,
2.用戶執行緒結束,守護執行緒自動自動結束,
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
//死回圈執行緒
public void run() {
int i=0;
while(true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+(i++));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.setName("分支執行緒");
t1.setDaemon(true);
t1.start();
//主執行緒執行代碼
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
Thread.sleep(1000);
}
}
}
執行結果:在主執行緒輸出完之后,守護執行緒死回圈也跟著結束了,
6.2 定時器
定時器有什么用呢?
目的是控制程式根據你設定的時間間隔去執行程式,
public static void main(String[] args) throws ParseException {
Timer timer=new Timer();
//日期類
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//獲取當前時間
Date firstTime= sdf.parse("2021-4-22 15:10:00");
timer.schedule(new LogTimerTask(),firstTime,1000);
}
}
//定時任務類
class LogTimerTask extends TimerTask{
public void run() {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=sdf.format(new Date());
System.out.println(time+"完成任務!");
}
}

小結:計時器可采用執行緒守護的方式,可以用上方法,還能用匿名內部類形式,
七、notify和wait方法
7.1 wait方法
Object o=new Object();
o.wait();
wait方法的作用是,讓o物件中正在運行的執行緒進入等待狀態并釋放掉o物件的鎖,無限期等待,直到執行緒被再次喚醒,
7.2 notify方法
Object o=new Object();
o.notify();
notify的方法的作用是:讓o物件進入等待狀態的執行緒“蘇醒”過來繼續執行,
還有一個,notifyAll方法是讓所有o物件的執行緒”蘇醒“過來,
notify和wait方法都是在synchronized基礎之上進行的,
7.3 兩種方法的使用
交替列印奇數偶數
public static void main(String[] args) {
Num num=new Num();
Thread t1=new Thread(new Os(num));
Thread t2=new Thread(new Js(num));
t1.start();
t2.start();
}
}
class Num{
int num=1;
public int getNum() {
return num;
}
public int printNum() {
return num++;
}
}
class Os implements Runnable{
Num num;
public Os(Num num) {
this.num=num;
}
public void run() {
while(num.getNum()<100) {
synchronized (num) {
if(num.getNum()%2==0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
num.notify();
}
}
}
}
class Js implements Runnable{
Num num;
public Js(Num num) {
this.num=num;
}
public void run() {
while(num.getNum()<100) {
synchronized (num) {
if(num.getNum()%2!=0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
num.notify();
}
}
}
}
小結:01執行緒輸出奇數,02執行緒輸出.當數字1進入到Thread-0執行緒,synchrozed會鎖住Num物件,進行判斷為false,輸出1,喚醒等待的thread-1執行緒執行,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/279366.html
標籤:其他
上一篇:Linux___執行緒互斥與同步
下一篇:暴力破解原理及實驗
