Java基礎(29)——多線程相關知識詳解及示例分析一(線程的基本概念與示例分析)
版權聲明
- 本文原創作者:清風不渡
- 博客地址:https://blog.csdn.net/WXKKang
一、進程和線程
1、基本概念
(1)進程
每個應用程序的執行單位就是一個進程,它包含了本應用程序運行時所需要的一切資源,例如:內存、CPU時間、IO等
在操作系統中,我們允許多個程序同時被加載到內存中,在操作系統的調度下,可以實現併發執行,每個用戶感覺到自己獨享CPU,這樣的設計大大提高了CPU的利用率
通過任務管理器可看到許多不同的進程,如下圖所示,我們發現只有運行的程序纔會出現進程,所以說:進程就是正在運行的程序。進程是系統進行資源分配和調用的獨立單位,每一個進程都有它自己的內存空間和系統資源
多進程的意義:單進程的計算機只能做一件事情,而我們現在的計算機都可以做多件事情。比如:一邊玩遊戲(遊戲進程),一邊聽音樂(音樂進程)。也就是說計算機都是支持多進程的,可以在一個時間段內執行多個任務從而提高CPU的使用率
但是,雖然我們可以在電腦上一邊聽音樂,一邊玩遊戲,但是兩者真的是同時進行的嗎?當然不是的,因爲單CPU在某一個時間點上只能處理一見事情,而一邊聽音樂,一邊玩遊戲則是CPU在做着程序間的高效切換(也就是在很短的時間內重複的來回切換程序)而讓使用者覺得不同的程序是同時進行的
(2)線程
一個線程和順序結構代碼的執行邏輯非常相似,它只有一個入口、一個出口以及一個順序執行的語句序列。從概念上說,一個線程的內部就是一個順序控制流
在現代操作系統中,每個進程中都可以提供多個線程併發運行。線程纔是程序中真正的執行體,進程只是一個應用程序中所有資源的分配單位
例如:我們使用微軟的Word編寫文檔,實際上就是同時運行了多個線程。這些線程中有的負責顯示,有的負責接受鍵盤輸入,有的進行自動存盤等等。當這些線程一起運行時,我們感覺到鍵盤輸入和屏幕顯示是同時運行的,而不是輸入一些字符之後過一段時間才能看到顯示,同時在後臺還自動執行保存文檔,這就是線程給我們帶來的方便之處
簡單來說,在同一個進程內又可以執行多個任務,而這每一個任務就可看作是一個線程
線程:程序的執行單元,又叫做執行路徑;它是程序使用CPU的最基本單位
單線程:程序只有一條執行路徑
多線程:程序有多條執行路徑
多線程存在的意義:
多線程的存在,不是提高程序的執行速度;而是爲了提高應用程序的使用率。程序的執行其實都是在搶CPU的資源,CPU 的執行權。多個進程是在搶CUP資源,而其中的某一個進程如果執行路徑比較多,就會有更高的機率搶到CPU的執行權。但是,在某個時刻我們不敢保證哪一個線程能夠搶到,所以線程的執行有隨機性!!!
假如計算機只有一個CPU,那麼CPU在某一個時刻只能執行一-條指令,線程只有得到CPU時間片,也就是使用權,纔可以執行指令。那麼Java是如何對線程進行調用的呢?
線程有兩種調度模型:
分時調度模型所有線程輪流使用 CPU的使用權,平均分配每個線程佔用CPU的時間片
搶佔式調度模型優先讓優先級高的線程使用CPU,如果線程的優先級相同,那麼會隨機選擇一個,優先級高的線程獲取的CPU時間片相對多一些
Java使用的是搶佔式調度模型
(3)進程與線程的關係
1、這是兩個不同層次上的概念,兩者的粒度不同。進程是操作系統來管理的,線程則是由進程來管理
2、不同進程的內存都是完全獨立的;一個進程內的不同線程則是共享本進程的內存和系統資源,不同線程之間有可能發生並生衝突
3、線程本身的數據通常只有寄存器數據,以及一個程序執行時使用的堆棧,所以線程的切換比進程切換的負擔要小
4、一個進程中的所有非守護線程都退出後,進程也會終止
5、不同線程之間是併發運行的,它們之間的運行順序是由操作系統調度的,是隨機的。但是,一個線程內部還是按照程序本身的流程順序執行,和線程調度是沒有關係的
2、JVM進程和線程
(1)Java程序和進程、線程
每個應用程序執行時都會創建一個對應的進程,在這個進程中擁有一個主線程(main),它是整個應用程序的執行入口
java.exe實現了Java 虛擬機,因此運行java.exe就會啓動一個新的虛擬機,實際上就是在本地操作系統中創建了一個JVM進程。如果同時運行多個java.exe,那麼操作系統將會爲每個java.exe分配一個獨立的JVM進程,這樣就會有多個JVM進程,它們互相獨立,互不干擾。
每個Java虛擬機運行後,都會自動啓動一個主線程( main thread)和垃圾回收線程,主線程首先加載有main方法的類,並自動調用main方法開始執行
(2)簡單的多線程實現
既然main函數是一個程序的入口,那麼一個Java程序在運行的時候就會在主線程中調用main方法,如果我們在main方法中創建新的線程,那麼這樣就可以實現一個多線程程序,示例如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
System.out.println("主線程[main]正在運行");
Thread thread = new TestThread();
thread.start();
//TestThread啓動後,主線程[main]將和其並行運行
}
}
class TestThread extends Thread{
@Override
public void run() {
// 線程執行體
System.out.println("TestThread正在運行");
}
}
這樣,就實現了一個簡單的多線程
二、線程編程
1、線程API
(1)Thread和Runnable
Runnable接口用於定義線程的執行體,其中僅僅聲明瞭一個run方法。Thread 實現了Runnable接口,但是它的run方法中沒有實現任何東西,需要在Thead子類中實現線程執行體
(2)創建線程的兩種方式
1、實現一個繼承Thread的子類,在子類中重寫run()方法
2、定義一個Runnable的實現類,通過Thread啓動新線程和執行這個實現類
2、使用Thread創建線程
Java本身沒有實現線程機制,而是通過Thread類封裝了底層操作系統的線程,爲Java提供了線程的支持
(1)API
Thread類中的主要方法如下:
(2)示例
現在我們來通過一個小的示例學習一下Thread類中的主要方法:
首先,我們創建一個Thread的子類,輸出當前線程的名字,代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class UserThread extends Thread{
public UserThread() {
}
//帶參構造函數
public UserThread(String threadName) {
super(threadName);
}
//重寫run方法
@Override
public void run() {
//調用printThreadName();方法
printThreadName();
}
public void printThreadName(){
for(int i=0;i<10;i++){
System.out.println(this.getName()+":"+i);
}
}
}
然後我們在Demo類中創建其對象並開啓線程,代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//創建對象
UserThread thread1 = new UserThread("張三");
UserThread thread2 = new UserThread("李四");
//普通調用run方法
thread1.run();
thread2.run();
//開啓線程
thread1.start();
thread2.start();
}
}
執行結果如下:
在本示例中就可以更好的看到多線程的搶佔式執行,開啓線程後兩個線程將開始競爭CPU時間,沒有固定的執行順序,這就證明多線程起到了效果
(3)線程的使用細節
1、Thread類的run()方法是一個空方法體,需要在子類中重寫run()方法實現處理邏輯
2、如果直接調用線程對象的run()方法,那麼JVM不會作爲一個新線程來運行,這只是一個普通的方法調用
3、使用Thread的start()方法啓動一個新線程
3、使用Runnable創建線程
Runnable接口用於定義線程的執行體,其中僅僅聲明瞭一個run方法。該方式可以避免由於Java單繼承帶來的侷限性。該方式適合多個相同程序的代碼去處理同一個資源的情況(例如賣票),把線程同程序的代碼,數據有效分離,較好的體現了面向對象的設計思想,其使用方法步驟如下:
(1)定義Runable的實現類
定義一個類MyRunnable實現Runnable接口,並且在run();方法中定義線程的執行體,代碼如下:
package qfbd.com;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyRunnable:"+i);
}
}
}
(2)啓動線程
啓動線程時,首先需要在Thread構造方法中使用Runnable實現類的對象作爲參數創建對象,調用Thread的start方法啓動一個新線程,這樣將會自動調用Runnable實現類的run方法
那麼,我們爲什麼需要將Runnable的實現類對象傳遞給Thread的構造函數呢?因爲Runnable接口的實現類對象不是線程類,它無法啓動一個新線程,只有Thread才能啓動一個新線程,代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//創建MyRunnable對象
MyRunnable myRunnable = new MyRunnable();
//使用Thread類構造函數傳入MyRunnable對象並創建線程對象
Thread thread = new Thread(myRunnable);
//開啓線程
thread.start();
//下面的for循環屬於main線程執行體
for (int i = 0; i < 100; i++) {
System.out.println("main:"+i);
}
}
}
執行結果如下:
4、線程常用方法
(1)常用API
注意:
1、針對不是Thread類的子類中如何獲取線程對象名稱呢?
public static Thread currentThread():返回當前正在執行的線程對象
Thread.currentThread().getName():返回當前正在執行的線程對象的名字
2、public static void sleep(long millis)方法中輸入參數的單位是毫秒
3、public final void join():等待該線程終止——該方法必須在其對應的start方法之後調用
4、public static void yield():暫停當前正在執行的線程對象,並執行其他線程。即調用該方法的線程會暫停,讓別的線程先執行。但是,該方法只能在一定程度上禮讓;即在一定程度上讓多個線程的執行更和諧,但是不能靠它保證每個線程輪次執行
5、public final void stop():讓線程停止;該方法已經過時,推薦不再使用
public void interrupt():中斷線程。中斷線程的方式:把線程的狀態終止,並拋出一個InterruptedException
(2)線程優先級
A、線程調度規則
Java提供一個線程調度器來監控程序中啓動後進入可運行狀態的所有線程。多個線程運行時,若線程的優先級相同,由操作系統按時間片輪轉方式和獨佔方式來分配線程的執行時間
線程調度器按照線程的優先級決定調度哪些線程來執行,具有高優先級的線程會在較低優先級的線程之前得到執行。同時線程的調度是搶先式的,即如果當前線程在執行過程中,一個具有更高優先級的線程進入可執行狀態,則該高優先級的線程會被立即調度執行
B、線程優先級
在Java中線程的優先級是用數字來表示的,分爲三個級別:
線程被創建後,其缺省的優先級是缺省優先級Thread.NORM_ PRIORITY,具有相同優先級的多個線程,若它們都爲高優先級Thread.MAX_ PRIORITY, 則每個線程都是獨佔式的,也就是說這些線程將被順序執行;若該優先級不爲高優先級,則這些線程將同時執行,也就是說這些線程的執行是無序的
垃圾回收線程的優先級是4
如何獲取線程對象的優先級呢?
public final int getPriority():返回線程對象的優先級
如何設置線程對象的優先級呢?
public final void setPriority(int newPriority):更改線程的優先級
注意:
線程默認優先級是5
線程優先級的範圍是: 1-10
線程優先級高僅僅表示線程獲取的CPU時間片的機率高,但是要在次數比較多,或者多次運行的時候才能看到比較好的效果。也就是說:不能保證優先級高的線程一直完全佔有執行權
(3)示例
線程實現類UserThread代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class UserThread extends Thread{
public UserThread() {
}
//帶參構造函數
public UserThread(String threadName) {
super(threadName);
}
//重寫run方法
@Override
public void run() {
int i = 0;
while(i<30){
i++;
try {
//線程睡眠
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//得到當前執行的線程對象
System.out.println("獲取的線程對象是否爲當前執行的:"+(Thread.currentThread() == this));
System.out.println("this.getName-->"+this.getName()+" "+",i="+i);
System.out.println("Thread.currentThread().getName();-->"+Thread.currentThread().getName()+" "+",i="+i);
}
}
}
測試類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
UserThread thread1 = new UserThread("線程1");
UserThread thread2 = new UserThread("線程2");
//查詢thread2線程名
System.out.println("當前線程名:"+thread2.getName());
//設置thread1線程名
thread1.setName("重新設置名稱的線程1");
//獲得優先級
System.out.println("thread1當前線程優先級爲:"+thread1.getPriority());
//設置線程優先級
thread1.setPriority(10);
System.out.println("thread1線程優先級設置後爲:"+thread1.getPriority());
//啓動線程
thread1.start();
thread2.start();
System.out.println("thread1線程是否活動:"+thread1.isAlive());
System.out.println("Hello World!");
}
}
執行此代碼可發現run();方法中每次循環都會使線程睡眠一段時間,執行的一部分結果如下:
好了就到這裏吧,下一篇我們將會到一個非常有意思的線程——守護線程,碼字不易,希望自己的文章能起到拋磚引玉的效果,大家多多點贊評論呦