java基礎學習總結——線程(一)
一、線程的基本概念
線程理解:線程是一個程序裏面不同的執行路徑
每一個分支都叫做一個線程,main()叫做主分支,也叫主線程。
程只是一個靜態的概念,機器上的一個.class文件,機器上的一個.exe文件,這個叫做一個進程。程序的執行過程都是這樣的:首先把程序的代碼放到內存的代碼區裏面,代碼放到代碼區後並沒有馬上開始執行,但這時候說明了一個進程準備開始,進程已經產生了,但還沒有開始執行,這就是進程,所以進程其實是一個靜態的概念,它本身就不能動。平常所說的進程的執行指的是進程裏面主線程開始執行了,也就是main()方法開始執行了。進程是一個靜態的概念,在我們機器裏面實際上運行的都是線程。
Windows操作系統是支持多線程的,它可以同時執行很多個線程,也支持多進程,因此Windows操作系統是支持多線程多進程的操作系統。Linux和Uinux也是支持多線程和多進程的操作系統。DOS就不是支持多線程和多進程了,它只支持單進程,在同一個時間點只能有一個進程在執行,這就叫單線程。
CPU難道真的很神通廣大,能夠同時執行那麼多程序嗎?不是的,CPU的執行是這樣的:CPU的速度很快,一秒鐘可以算好幾億次,因此CPU把自己的時間分成一個個小時間片,我這個時間片執行你一會,下一個時間片執行他一會,再下一個時間片又執行其他人一會,雖然有幾十個線程,但一樣可以在很短的時間內把他們通通都執行一遍,但對我們人來說,CPU的執行速度太快了,因此看起來就像是在同時執行一樣,但實際上在一個時間點上,CPU只有一個線程在運行。
學習線程首先要理清楚三個概念:
- 進程:進程是一個靜態的概念
- 線程:一個進程裏面有一個主線程叫main()方法,是一個程序裏面的,一個進程裏面不同的執行路徑。
- 在同一個時間點上,一個CPU只能支持一個線程在執行。因爲CPU運行的速度很快,因此我們看起來的感覺就像是多線程一樣。
什麼纔是真正的多線程?如果你的機器是雙CPU,或者是雙核,這確確實實是多線程。
二、線程的創建和啓動
在JAVA裏面,JAVA的線程是通過java.lang.Thread類來實現的,每一個Thread對象代表一個新的線程。創建一個新線程出來有兩種方法:第一個是從Thread類繼承,另一個是實現接口runnable。VM啓動時會有一個由主方法(public static void main())所定義的線程,這個線程叫主線程。可以通過創建Thread的實例來創建新的線程。你只要new一個Thread對象,一個新的線程也就出現了。每個線程都是通過某個特定的Thread對象所對應的方法run()來完成其操作的,方法run()稱爲線程體。
範例1:使用實現Runnable接口創建和啓動新線程
開闢一個新的線程來調用run方法
package cn.galc.test;
public class TestThread1{
public static void main(String args[]){
Runner1 r1 = new Runner1();//這裏new了一個線程類的對象出來
//r1.run();//這個稱爲方法調用,方法調用的執行是等run()方法執行完之後纔會繼續執行main()方法
Thread t = new Thread(r1);//要啓動一個新的線程就必須new一個Thread對象出來
//這裏使用的是Thread(Runnable target) 這構造方法
t.start();//啓動新開闢的線程,新線程執行的是run()方法,新線程與主線程會一起並行執行
for(int i=0;i<10;i++){
System.out.println("maintheod:"+i);
}
}
}
/*定義一個類用來實現Runnable接口,實現Runnable接口就表示這個類是一個線程類*/
class Runner1 implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println("Runner1:"+i);
}
}
}
多線程程序執行的過程如下所示:
不開闢新線程直接調用run方法
運行結果如下:
範例2:繼承Thread類,並重寫其run()方法創建和啓動新的線程
package cn.galc.test;
/*線程創建與啓動的第二種方法:定義Thread的子類並實現run()方法*/
public class TestThread2{
public static void main(String args[]){
Runner2 r2 = new Runner2();
r2.start();//調用start()方法啓動新開闢的線程
for(int i=0;i<=10;i++){
System.out.println("mainMethod:"+i);
}
}
}
/*Runner2類從Thread類繼承
通過實例化Runner2類的一個對象就可以開闢一個新的線程
調用從Thread類繼承來的start()方法就可以啓動新開闢的線程*/
class Runner2 extends Thread{
public void run(){//重寫run()方法的實現
for(int i=0;i<=10;i++){
System.out.println("Runner2:"+i);
}
}
}
使用實現Runnable接口和繼承Thread類這兩種開闢新線程的方法的選擇應該優先選擇實現Runnable接口這種方式去開闢一個新的線程。因爲接口的實現可以實現多個,而類的繼承只能是單繼承。因此在開闢新線程時能夠使用Runnable接口就儘量不要使用從Thread類繼承的方式來開闢新的線程。
三、線程狀態轉換
3.1.線程控制的基本方法
3.2. sleep/join/yield方法介紹
sleep方法的應用範例:
package cn.galc.test;
import java.util.*;
public class TestThread3 {
public static void main(String args[]){
MyThread thread = new MyThread();
thread.start();//調用start()方法啓動新開闢的線程
try {
/*Thread.sleep(10000);
sleep()方法是在Thread類裏面聲明的一個靜態方法,因此可以使用Thread.sleep()的格式進行調用
*/
/*MyThread.sleep(10000);
MyThread類繼承了Thread類,自然也繼承了sleep()方法,所以也可以使用MyThread.sleep()的格式進行調用
*/
/*靜態方法的調用可以直接使用“類名.靜態方法名”
或者“對象的引用.靜態方法名”的方式來調用*/
MyThread.sleep(10000);
System.out.println("主線程睡眠了10秒種後再次啓動了");
//在main()方法裏面調用另外一個類的靜態方法時,需要使用“靜態方法所在的類.靜態方法名”這種方式來調用
/*
所以這裏是讓主線程睡眠10秒種
在哪個線程裏面調用了sleep()方法就讓哪個線程睡眠,所以現在是主線程睡眠了。
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread.interrupt();//使用interrupt()方法去結束掉一個線程的執行並不是一個很好的做法
thread.flag=false;//改變循環條件,結束死循環
/**
* 當發生InterruptedException時,直接把循環的條件設置爲false即可退出死循環,
* 繼而結束掉子線程的執行,這是一種比較好的結束子線程的做法
*/
/**
* 調用interrupt()方法把正在運行的線程打斷
相當於是主線程一盆涼水潑上去把正在執行分線程打斷了
分線程被打斷之後就會拋InterruptedException異常,這樣就會執行return語句返回,結束掉線程的執行
所以這裏的分線程在執行完10秒鐘之後就結束掉了線程的執行
*/
}
}
class MyThread extends Thread {
boolean flag = true;// 定義一個標記,用來控制循環的條件
public void run() {
/*
* 注意:這裏不能在run()方法的後面直接寫throw Exception來拋異常,
* 因爲現在是要重寫從Thread類繼承而來的run()方法,重寫方法不能拋出比被重寫的方法的不同的異常。
* 所以這裏只能寫try……catch()來捕獲異常
*/
while (flag) {
System.out.println("==========" + new Date().toLocaleString() + "===========");
try {
/*
* 靜態方法的調用格式一般爲“類名.方法名”的格式去調用 在本類中聲明的靜態方法時調用時直接寫靜態方法名即可。 當然使用“類名.方法名”的格式去調用也是沒有錯的
*/
// MyThread.sleep(1000);//使用“類名.方法名”的格式去調用屬於本類的靜態方法
sleep(1000);//睡眠的時如果被打斷就會拋出InterruptedException異常
// 這裏是讓這個新開闢的線程每隔一秒睡眠一次,然後睡眠一秒鐘後再次啓動該線程
// 這裏在一個死循環裏面每隔一秒啓動一次線程,每個一秒打印出當前的系統時間
} catch (InterruptedException e) {
/*
* 睡眠的時一盤冷水潑過來就有可能會打斷睡眠
* 因此讓正在運行線程被一些意外的原因中斷的時候有可能會拋被打擾中斷(InterruptedException)的異常
*/
return;
// 線程被中斷後就返回,相當於是結束線程
}
}
}
}
運行結果:
join方法的使用範例:
package cn.galc.test;
public class TestThread4 {
public static void main(String args[]) {
MyThread2 thread2 = new MyThread2("mythread");
// 在創建一個新的線程對象的同時給這個線程對象命名爲mythread
thread2.start();// 啓動線程
try {
thread2.join();// 調用join()方法合併線程,將子線程mythread合併到主線程裏面
// 合併線程後,程序的執行的過程就相當於是方法的調用的執行過程
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i <= 5; i++) {
System.out.println("I am main Thread");
}
}
}
class MyThread2 extends Thread {
MyThread2(String s) {
super(s);
/*
* 使用super關鍵字調用父類的構造方法
* 父類Thread的其中一個構造方法:“public Thread(String name)”
* 通過這樣的構造方法可以給新開闢的線程命名,便於管理線程
*/
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("I am a\t" + getName());
// 使用父類Thread裏面定義的
//public final String getName(),Returns this thread's name.
try {
sleep(1000);// 讓子線程每執行一次就睡眠1秒鐘
} catch (InterruptedException e) {
return;
}
}
}
}
運行結果:
yield方法的使用範例:
package cn.galc.test;
public class TestThread5 {
public static void main(String args[]) {
MyThread3 t1 = new MyThread3("t1");
/* 同時開闢了兩條子線程t1和t2,t1和t2執行的都是run()方法 */
/* 這個程序的執行過程中總共有3個線程在並行執行,分別爲子線程t1和t2以及主線程 */
MyThread3 t2 = new MyThread3("t2");
t1.start();// 啓動子線程t1
t2.start();// 啓動子線程t2
for (int i = 0; i <= 5; i++) {
System.out.println("I am main Thread");
}
}
}
class MyThread3 extends Thread {
MyThread3(String s) {
super(s);
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + ":" + i);
if (i % 2 == 0) {
yield();// 當執行到i能被2整除時當前執行的線程就讓出來讓另一個在執行run()方法的線程來優先執行
/*
* 在程序的運行的過程中可以看到,
* 線程t1執行到(i%2==0)次時就會讓出線程讓t2線程來優先執行
* 而線程t2執行到(i%2==0)次時也會讓出線程給t1線程優先執行
*/
}
}
}
}
運行結果如下: