進程與線程
進程是程序的一次動態執行過程,它需要經歷從代碼加載,代碼執行到執行完畢的一個完整的過程,這個過程也是進程本身從產生,發展到最終消亡的過程。多進程操作系統能同時達運行多個進程(程序),由於 CPU 具備分時機制,所以每個進程都能循環獲得自己的CPU 時間片。由於 CPU 執行速度非常快,使得所有程序好像是在同時運行一樣。
多線程是實現併發機制的一種有效手段。進程和線程一樣,都是實現併發的一個基本單位。線程是比進程更小的執行單位,線程是進程的基礎之上進行進一步的劃分。所謂多線程是指一個進程在執行過程中可以產生多個更小的程序單元,這些更小的單元稱爲線程,這些線程可以同時存在,同時運行,一個進程可能包含多個同時執行的線程。進程與線程的區別如圖所示:
Java中線程實現的方式
在 Java 中實現多線程有兩種手段,一種是繼承 Thread 類,另一種就是實現 Runnable 接口。下面我們就分別來介紹這兩種方式的使用。
實現 Runnable 接口
package ljz;
class MyThread implements Runnable{ // 實現Runnable接口,作爲線程的實現類
private String name ; // 表示線程的名稱
public MyThread(String name){
this.name = name ; // 通過構造方法配置name屬性
}
public void run(){ // 覆寫run()方法,作爲線程 的操作主體
for(int i=0;i<10;i++){
System.out.println(name + "運行,i = " + i) ;
}
}
};
public class RunnableDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("線程A ") ; // 實例化對象
MyThread mt2 = new MyThread("線程B ") ; // 實例化對象
Thread t1 = new Thread(mt1) ; // 實例化Thread類對象
Thread t2 = new Thread(mt2) ; // 實例化Thread類對象
t1.start() ; // 啓動多線程
t2.start() ; // 啓動多線程
}
};
程序運行結果:
繼承 Thread 類
class MyThread extends Thread{ // 繼承Thread類,作爲線程的實現類
private String name ; // 表示線程的名稱
public MyThread(String name){
this.name = name ; // 通過構造方法配置name屬性
}
public void run(){ // 覆寫run()方法,作爲線程 的操作主體
for(int i=0;i<10;i++){
System.out.println(name + "運行,i = " + i) ;
}
}
};
public class ThreadDemo02{
public static void main(String args[]){
MyThread mt1 = new MyThread("線程A ") ; // 實例化對象
MyThread mt2 = new MyThread("線程B ") ; // 實例化對象
mt1.start() ; // 調用線程主體
mt2.start() ; // 調用線程主體
}
};
程序運行結果:
從程序可以看出,現在的兩個線程對象是交錯運行的,哪個線程對象搶到了 CPU 資源,哪個線程就可以運行,所以程序每次的運行結果肯定是不一樣的,在線程啓動雖然調用的是 start() 方法,但實際上調用的卻是 run() 方法定義的主體。
Thread 類和 Runnable 接口
通過 Thread 類和 Runable 接口都可以實現多線程,那麼兩者有哪些聯繫和區別呢?下面我們觀察 Thread 類的定義。
public class Thread extends Object implements Runnable
從 Thread 類的定義可以清楚的發現,Thread 類也是 Runnable 接口的子類,但在Thread類中並沒有完全實現 Runnable 接口中的 run() 方法,下面是 Thread 類的部分定義。
Private Runnable target;
public Thread(Runnable target,String name){
init(null,target,name,0);
}
private void init(ThreadGroup g,Runnable target,String name,long stackSize){
...
this.target=target;
}
public void run(){
if(target!=null){
target.run();
}
}
從定義中可以發現,在 Thread 類中的 run() 方法調用的是 Runnable 接口中的 run() 方法,也就是說此方法是由 Runnable 子類完成的,所以如果要通過繼承 Thread 類實現多線程,則必須覆寫 run()。
實際上 Thread 類和 Runnable 接口之間在使用上也是有區別的,如果一個類繼承 Thread類,則不適合於多個線程共享資源,而實現了 Runnable 接口,就可以方便的實現資源的共享。
線程的狀態變化
要想實現多線程,必須在主線程中創建新的線程對象。任何線程一般具有5種狀態,即創建,就緒,運行,阻塞,終止。下面分別介紹一下這幾種狀態:
-
創建狀態
在程序中用構造方法創建了一個線程對象後,新的線程對象便處於新建狀態,此時它已經有了相應的內存空間和其他資源,但還處於不可運行狀態。新建一個線程對象可採用Thread 類的構造方法來實現,例如 “Thread thread=new Thread()”。
-
就緒狀態
新建線程對象後,調用該線程的 start() 方法就可以啓動線程。當線程啓動時,線程進入就緒狀態。此時,線程將進入線程隊列排隊,等待 CPU 服務,這表明它已經具備了運行條件。
-
運行狀態
當就緒狀態被調用並獲得處理器資源時,線程就進入了運行狀態。此時,自動調用該線程對象的 run() 方法。run() 方法定義該線程的操作和功能。
-
阻塞狀態
一個正在執行的線程在某些特殊情況下,如被人爲掛起或需要執行耗時的輸入/輸出操作,會讓 CPU 暫時中止自己的執行,進入阻塞狀態。在可執行狀態下,如果調用sleep(),suspend(),wait() 等方法,線程都將進入阻塞狀態,發生阻塞時線程不能進入排隊隊列,只有當引起阻塞的原因被消除後,線程纔可以轉入就緒狀態。
-
死亡狀態
線程調用 stop() 方法時或 run() 方法執行結束後,即處於死亡狀態。處於死亡狀態的線程不具有繼續運行的能力。
在此提出一個問題,Java 程序每次運行至少啓動幾個線程?
回答:至少啓動兩個線程,每當使用 Java 命令執行一個類時,實際上都會啓動一個 JVM,每一個JVM實際上就是在操作系統中啓動一個線程,Java 本身具備了垃圾的收集機制。所以在 Java 運行時至少會啓動兩個線程,一個是 main 線程,另外一個是垃圾收集線程。
取得和設置線程的名稱
class MyThread implements Runnable{ //實現Runnable接口
public void run(){
for(int i=0;i<3;i++){
System.Out.Println(Thread.currentThread().getName()+"運行, i="+i); //取得當前線程的名稱
}
}
};
public class ThreadDemo{
public static void main(String args[]){
MyThread my=new MyThread(); //定義Runnable子類對象
new Thread(my).start; //系統自動設置線程名稱
new Thread(my,"線程A").start(); //手工設置線程名稱
}
};
程序運行結果:
線程的操作方法
剛纔在分析自定義模式工作原理的時候其實就已經提到了,如果想要更改Glide的默認配
線程的強制運行
在線程操作中,可以使用 join() 方法讓一個線程強制運行,線程強制運行期間,其他線程無法運行,必須等待此線程完成之後纔可以繼續執行。
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()
+ "運行,i = " + i) ; // 取得當前線程的名字
}
}
};
public class ThreadJoinDemo{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 實例化Runnable子類對象
Thread t = new Thread(mt,"線程"); // 實例化Thread對象
t.start() ; // 啓動線程
for(int i=0;i<50;i++){
if(i>10){
try{
t.join() ; // 線程強制運行
}catch(InterruptedException e){
}
}
System.out.println("Main線程運行 --> " + i) ;
}
}
};
程序運行結果:
線程的休眠
在程序中允許一個線程進行暫時的休眠,直接使用 Thread.sleep() 即可實現休眠。
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
for(int i=0;i<50;i++){
try{
Thread.sleep(500) ; // 線程休眠
}catch(InterruptedException e){
}
System.out.println(Thread.currentThread().getName()
+ "運行,i = " + i) ; // 取得當前線程的名字
}
}
};
public class ThreadSleepDemo{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 實例化Runnable子類對象
Thread t = new Thread(mt,"線程"); // 實例化Thread對象
t.start() ; // 啓動線程
}
};
程序執行結果:
中斷線程
當一個線程運行時,另外一個線程可以直接通過interrupt()方法中斷其運行狀態。
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
System.out.println("1、進入run()方法") ;
try{
Thread.sleep(10000) ; // 線程休眠10秒
System.out.println("2、已經完成了休眠") ;
}catch(InterruptedException e){
System.out.println("3、休眠被終止") ;
return ; // 返回調用處
}
System.out.println("4、run()方法正常結束") ;
}
};
public class ThreadInterruptDemo{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 實例化Runnable子類對象
Thread t = new Thread(mt,"線程"); // 實例化Thread對象
t.start() ; // 啓動線程
try{
Thread.sleep(2000) ; // 線程休眠2秒
}catch(InterruptedException e){
System.out.println("3、休眠被終止") ;
}
t.interrupt() ; // 中斷線程執行
}
};
程序運行結果是:
後臺線程
在 Java 程序中,只要前臺有一個線程在運行,則整個 Java 進程都不會消失,所以此時可以設置一個後臺線程,這樣即使 Java 線程結束了,此後臺線程依然會繼續執行,要想實現這樣的操作,直接使用 setDaemon() 方法即可。
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
while(true){
System.out.println(Thread.currentThread().getName() + "在運行。") ;
}
}
};
public class ThreadDaemonDemo{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 實例化Runnable子類對象
Thread t = new Thread(mt,"線程"); // 實例化Thread對象
t.setDaemon(true) ; // 此線程在後臺運行
t.start() ; // 啓動線程
}
};
在線程類 MyThread 中,儘管 run() 方法中是死循環的方式,但是程序依然可以執行完,因爲方法中死循環的線程操作已經設置成後臺運行。
線程的優先級
在 Java 的線程操作中,所有的線程在運行前都會保持在就緒狀態,那麼此時,哪個線程的優先級高,哪個線程就有可能會先被執行。
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
for(int i=0;i<5;i++){
try{
Thread.sleep(500) ; // 線程休眠
}catch(InterruptedException e){
}
System.out.println(Thread.currentThread().getName()
+ "運行,i = " + i) ; // 取得當前線程的名字
}
}
};
public class ThreadPriorityDemo{
public static void main(String args[]){
Thread t1 = new Thread(new MyThread(),"線程A") ; // 實例化線程對象
Thread t2 = new Thread(new MyThread(),"線程B") ; // 實例化線程對象
Thread t3 = new Thread(new MyThread(),"線程C") ; // 實例化線程對象
t1.setPriority(Thread.MIN_PRIORITY) ; // 優先級最低
t2.setPriority(Thread.MAX_PRIORITY) ; // 優先級最高
t3.setPriority(Thread.NORM_PRIORITY) ; // 優先級最中等
t1.start() ; // 啓動線程
t2.start() ; // 啓動線程
t3.start() ; // 啓動線程
}
};
程序運行結果:
從程序的運行結果中可以觀察到,線程將根據其優先級的大小來決定哪個線程會先運行,但是需要注意並非優先級越高就一定會先執行,哪個線程先執行將由 CPU 的調度決定。
線程的禮讓
在線程操作中,也可以使用 yield() 方法將一個線程的操作暫時讓給其他線程執行
class MyThread implements Runnable{ // 實現Runnable接口
public void run(){ // 覆寫run()方法
for(int i=0;i<5;i++){
try{
Thread.sleep(500) ;
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()
+ "運行,i = " + i) ; // 取得當前線程的名字
if(i==2){
System.out.print("線程禮讓:") ;
Thread.currentThread().yield() ; // 線程禮讓
}
}
}
};
public class ThreadYieldDemo{
public static void main(String args[]){
MyThread my = new MyThread() ; // 實例化MyThread對象
Thread t1 = new Thread(my,"線程A") ;
Thread t2 = new Thread(my,"線程B") ;
t1.start() ;
t2.start() ;
}
};
程序執行結果:
同步以及死鎖
一個多線程的程序如果是通過 Runnable 接口實現的,則意味着類中的屬性被多個線程共享,那麼這樣就會造成一種問題,如果這多個線程要操作同一個資源時就有可能出現資源同步問題。
解決方法:
同步代碼塊
synchronized(同步對象){
需要同步的代碼
}
class MyThread implements Runnable{
private int ticket = 5 ; // 假設一共有5張票
public void run(){
for(int i=0;i<100;i++){
synchronized(this){ // 要對當前對象進行同步
if(ticket>0){ // 還有票
try{
Thread.sleep(300) ; // 加入延遲
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println("賣票:ticket = " + ticket-- );
}
}
}
}
};
public class SyncDemo02{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 定義線程對象
Thread t1 = new Thread(mt) ; // 定義Thread對象
Thread t2 = new Thread(mt) ; // 定義Thread對象
Thread t3 = new Thread(mt) ; // 定義Thread對象
t1.start() ;
t2.start() ;
t3.start() ;
}
};
程序執行結果:
同步方法
除了可以將需要的代碼設置成同步代碼塊外,也可以使用 synchronized 關鍵字將一個方法聲明爲同步方法。
synchronized 方法返回值 方法名稱(參數列表){
}
class MyThread implements Runnable{
private int ticket = 5 ; // 假設一共有5張票
public void run(){
for(int i=0;i<100;i++){
this.sale() ; // 調用同步方法
}
}
public synchronized void sale(){ // 聲明同步方法
if(ticket>0){ // 還有票
try{
Thread.sleep(300) ; // 加入延遲
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println("賣票:ticket = " + ticket-- );
}
}
};
public class SyncDemo03{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 定義線程對象
Thread t1 = new Thread(mt) ; // 定義Thread對象
Thread t2 = new Thread(mt) ; // 定義Thread對象
Thread t3 = new Thread(mt) ; // 定義Thread對象
t1.start() ;
t2.start() ;
t3.start() ;
}
};
程序執行結果:
從程序運行的結果可以發現,此代碼完成了與之前同步代碼同樣的功能。
死鎖
同步可以保證資源共享操作的正確性,但是過多同步也會產生問題。例如,現在張三想要李四的畫,李四想要張三的書,張三對李四說“把你的畫給我,我就給你書”,李四也對張三說“把你的書給我,我就給你畫”兩個人互相等對方先行動,就這麼幹等沒有結果,這實際上就是死鎖的概念。
所謂死鎖,就是兩個線程都在等待對方先完成,造成程序的停滯,一般程序的死鎖都是在程序運行時出現的。
下面以一個簡單範例說明這個概念
class Zhangsan{ // 定義張三類
public void say(){
System.out.println("張三對李四說:“你給我畫,我就把書給你。”") ;
}
public void get(){
System.out.println("張三得到畫了。") ;
}
};
class Lisi{ // 定義李四類
public void say(){
System.out.println("李四對張三說:“你給我書,我就把畫給你”") ;
}
public void get(){
System.out.println("李四得到書了。") ;
}
};
public class ThreadDeadLock implements Runnable{
private static Zhangsan zs = new Zhangsan() ; // 實例化static型對象
private static Lisi ls = new Lisi() ; // 實例化static型對象
private boolean flag = false ; // 聲明標誌位,判斷那個先說話
public void run(){ // 覆寫run()方法
if(flag){
synchronized(zs){ // 同步張三
zs.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(ls){
zs.get() ;
}
}
}else{
synchronized(ls){
ls.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(zs){
ls.get() ;
}
}
}
}
public static void main(String args[]){
ThreadDeadLock t1 = new ThreadDeadLock() ; // 控制張三
ThreadDeadLock t2 = new ThreadDeadLock() ; // 控制李四
t1.flag = true ;
t2.flag = false ;
Thread thA = new Thread(t1) ;
Thread thB = new Thread(t2) ;
thA.start() ;
thB.start() ;
}
};
程序運行結果:
以下代碼不再執行,程序進入死鎖狀態。
總結
至此關於多線程一些基本操作就介紹完了,鑑於筆者經驗有限,如果有什麼不足和缺漏的地方,歡迎相互交流學習,感謝大家!