一. 進程和線程
1. 什么是進程
進程是處于運行中的程序,并且具有一定的獨立功能,進程是系統(tǒng)進行資源分配和調(diào)度的獨立單位。
2. 進程的三個特征
(1)獨立性。進程是系統(tǒng)中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有進過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。
(2)動態(tài)性。進程與程序的區(qū)別在于,程序只是一個靜態(tài)的指令集合,而進程是一個正在系統(tǒng)中活動的指令集合,在進程中增加了時間的概念。進程具有自己的生命周期和各種不同的狀態(tài),這些概念在程序中都是不具備的。
(3)并發(fā)性。多個進程可以在單個處理器上并發(fā)的執(zhí)行,多個進程之間不會互相影響。
操作系統(tǒng)多進程支持的理解:程序指令通過cpu執(zhí)行,在某個時間點只有一個程序的指令得到執(zhí)行,但是cpu執(zhí)行指令的速度非???,所以多個程序指令在cpu上輪流切換執(zhí)行的速度也很快,這樣在宏觀上感覺是多個程序在并發(fā)的執(zhí)行??梢赃@樣理解程序 的并發(fā),宏觀上并發(fā)執(zhí)行,微觀上順序執(zhí)行。
3. 什么是線程
線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須擁有一個父進程。線程可以擁有自己的的堆棧、自己的程序計數(shù)器和自己的局部變量,但不擁有系統(tǒng)資源,它與父進程的其他線程共享該進程的所擁有的全部資源。
二.創(chuàng)建線程的3種方式
1. 繼承Thread類創(chuàng)建線程
步驟如下:
a.定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務。
b.創(chuàng)建Thread子類實例,即創(chuàng)建了線程對象。
c.調(diào)用線程對象的start()方法來創(chuàng)建并啟動線程。
public class FirstThread extends Thread{
private int i;
//重寫run()方法,run()方法的方法體就是線程的執(zhí)行體
public void run(){
for(;i<100;i++){
// 當線程類繼承Thread類時,直接使用this即可獲取當前線程
//Thread對象的getName()方法返回當前線程的名字
//因此可以直接調(diào)用getName()方法返回當前線程的名字
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
//調(diào)用Thread類的currentThread()方法獲取當前線程
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
//創(chuàng)建并啟動第一個線程
new FirstThread().start();
//創(chuàng)建并啟動第二個線程
new FirstThread().start();
}
}
}
}
2. 實現(xiàn)Runnable接口創(chuàng)建線程類
a.定義Runnable接口的實現(xiàn)類,并重寫該接口的run()方法,該run()方法的方法體就是線程的線程執(zhí)行體。
b.創(chuàng)建Runnable實現(xiàn)類的實例,并以此實例作為Thread類的target來創(chuàng)建Thread對象,該Thread對象才是真正的線程對象。
public class SecondThread implements Runnable{
private int i;
//run()方法同樣是線程的執(zhí)行體
@Override
public void run(){
for(;i<100;i++){
//當線程實現(xiàn)Runnable接口時
//如果想獲取當前線程,只能通過Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondThread st=new SecondThread();
//通過new Thread(target,name)方法創(chuàng)建新線程
new Thread(st, "新線程1").start();
new Thread(st, "新線程2").start();
}
}
}
}
3. 使用Callable和Future創(chuàng)建線程
a.創(chuàng)建Callable接口的實現(xiàn)類,并實現(xiàn)call()方法,該call()方法將作為線程的執(zhí)行體,且該call()方法有返回值,再創(chuàng)建Callable實現(xiàn)類的實例。
b.使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
c.使用FutureTask對象作為Thread對象的target創(chuàng)建并啟動新線程。
d.使用FutureTask對象的get()方法來獲得子線程執(zhí)行結(jié)束后的返回值。
public class ThridThread {
public static void main(String[] args) {
//創(chuàng)建Callable對象
ThridThread rt=new ThridThread();
//先使用Lambda表達式創(chuàng)建Callable<Integer>對象
//使用FutureTask來包裝Callable對象
FutureTask<Integer> task=new FutureTask<>((Callable<Integer>)()->{
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
//call()方法的返回值
return i;
});
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"循環(huán)變量i的值: "+i);
if(i==20){
//實質(zhì)是以Callable對象來創(chuàng)建并啟動線程
new Thread(task,"有返回值的線程:").start();;
}
}
try{
//獲取線程的返回值
System.out.println("子線程的返回值:"+task.get());
}
catch (Exception e) {
e.printStackTrace();
}
}
}
三.線程的生命周期
每個線程都要經(jīng)歷新建、就緒、運行、阻塞、死亡5種狀態(tài)。
新建狀態(tài):就是通過new關鍵字創(chuàng)建線程對象時的狀態(tài)。
就緒狀態(tài):即通過線程對象的start()方法啟動線程時對應的狀態(tài),此時線程并不一定馬上能進入運行狀態(tài),線程的運行由操作系統(tǒng)的調(diào)度程序進行線程的調(diào)度。
運行狀態(tài):是指線程獲得cpu的執(zhí)行權(quán),線程正在執(zhí)行需要執(zhí)行的代碼。
-
阻塞狀態(tài):當發(fā)生以下情況時線程進入阻塞狀態(tài)。
- 線程調(diào)用sleep()方法主動放棄所占有的處理器資源。
- 線程調(diào)用了一個阻塞式IO方法,在方法返回之前線程阻塞。
- 線程試圖獲得一個同步監(jiān)視器,但該監(jiān)視器正在被其他線程所持有。
- 線程正在等待某個通知(notify)。
- 程序調(diào)用了線程的suspend()方法將該線程掛起。
-
死亡狀態(tài):線程會以以下三種方式結(jié)束,結(jié)束后的線程處于死亡狀態(tài)。
- run()方法和call()方法執(zhí)行完成,線程正常結(jié)束。
- 線程拋出一個未捕獲的Exception或Error。
- 直接調(diào)用線程的stop()方法來結(jié)束線程。
線程的狀態(tài)轉(zhuǎn)換圖如下:
注意點:
- 啟動一個線程是使用線程對象的start()方法,而不是直接調(diào)用run()方法。如果直接調(diào)用run()方法,則和普通的對象調(diào)用實例方法一樣,沒有啟動一個線程來執(zhí)行該方法。啟動一個線程只能對處于新建狀態(tài)的線程啟動,調(diào)用處于新建狀態(tài)的線程對象使用start()方法來啟動一個線程。如果對處于新建狀態(tài)的線程對象調(diào)用了run()方法或其他方法,則此線程對象就不再處于新建狀態(tài)了,以后調(diào)用該線程對象的start()方法將不會啟動一個線程。
- 對于處于死亡狀態(tài)的線程不能再次調(diào)用該線程的start()方法。程序只能對處于新建狀態(tài)的線程調(diào)用start()方法,對新建狀態(tài)的線程兩次調(diào)用start()方法也是錯誤的,會引起IllegalThreadStateException異常。
四.控制線程
1. join線程
join()方法時Java的Thread類提供的讓一個線程等待另一個線程完成的方法。當在某個程序的執(zhí)行流中調(diào)用其他線程的join()方法時,調(diào)用線程將被阻塞,直到被join()方法加入的join線程執(zhí)行完成為止。
public class JoinThread extends Thread {
//提供一個有參構(gòu)造器,用于設置該線程的名字
public JoinThread(String name){
super(name);
}
//重寫run方法,定義線程的執(zhí)行體
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<100;i++){
if(i==20){
JoinThread jt=new JoinThread("被join的線程");
jt.start();
//main線程調(diào)用了jt線程的join()方法,main線程必須等待jt線程執(zhí)行結(jié)束才會向下執(zhí)行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
2. 后臺線程
后臺線程(Daemon Thread)是在后臺運行的,它的任務是為其他的線程提供服務,也被稱為守護線程或精靈線程。
后臺線程的特征:如果所有的前臺線程都死亡,后臺線程會自動死亡。
調(diào)用Thread對象的setDaemon(true)方法可以將一個指定的線程設置為后臺線程。
public class DaemonThread extends Thread {
public void run(){
for(int i=0;i<1000;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
DaemonThread dt=new DaemonThread();
//將此線程設置為后臺線程
dt.setDaemon(true);
dt.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
//程序執(zhí)行到此處,前臺線程main線程結(jié)束
//后臺線程也應該隨之結(jié)束
}
}
3. 線程睡眠:sleep
Thread類的sleep()方法用來暫停線程的執(zhí)行,調(diào)用sleep()的線程將會進入阻塞狀態(tài)。Thread類的sleep()方法是Thread類的靜態(tài)方法。
public class SleepTest {
public static void main(String[] args) throws Exception {
for(int i=0;i<10;i++){
System.out.println("當前時間:"+new Date());
//調(diào)用sleep()方法讓當前線程暫停1s
Thread.sleep(1000);
}
}
}
4. 線程讓步:yield
yield()方法也是Thread類提供的靜態(tài)方法,讓線程暫停執(zhí)行,與sleep()方法不同的是,yeild()方法不會將線程阻塞,當某個線程調(diào)用了yield()方法時,該線程會暫停執(zhí)行進入就緒狀態(tài),只有優(yōu)先級與當前線程相同或者優(yōu)先級比當前線程高的處于就緒狀態(tài)的線程才會獲得執(zhí)行的機會。
public class YieldTest extends Thread {
public YieldTest(String name){
super(name);
}
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
//當i=20時,使用yield()方法讓當前線程讓步
if(i==20){
Thread.yield();
}
}
}
public static void main(String[] args) {
//啟動兩個并發(fā)線程
YieldTest yt1=new YieldTest("高級");
//將yt1線程的設置為最高優(yōu)先級
//yt1.setPriority(MAX_PRIORITY);
yt1.start();
YieldTest yt2=new YieldTest("低級");
//將yt2線程的設置為最低優(yōu)先級
//yt2.setPriority(MIN_PRIORITY);
yt2.start();
}
執(zhí)行上面的程序?qū)吹皆趇=20的時候yt1線程執(zhí)行yield()方法,因為yt2線程與yt1線程處于同一優(yōu)先級別,所以yt2線程將會獲得執(zhí)行權(quán),然后在yt2執(zhí)行到i=20的時候,yt2調(diào)用線程讓步方法yeild(),同樣的原因線程yt1將會獲得執(zhí)行權(quán)。
5. 改變線程的優(yōu)先級
Java中每個線程都有一定的優(yōu)先級,優(yōu)先級高的線程獲得執(zhí)行的機會多,而優(yōu)先級低的線程獲得執(zhí)行的機會少。對于創(chuàng)建的線程,Java默認的優(yōu)先級同創(chuàng)建它的父線程的優(yōu)先級相同。如果想改變線程的優(yōu)先級,則可以使用Thread類提供的setPriority(int newPriority)方法設置線程的優(yōu)先級,而getPriority()方法返回線程的優(yōu)先級。Java中的優(yōu)先級的參數(shù)范圍是1-10的整數(shù)。