java多線程與線程間通信


 本文學習並總結java多線程與線程間通信的原理和方法,內容涉及java線程的衆多常見重要知識點,學習後會對java多線程概念及線程間通信方式有直觀清晰的瞭解和掌握,可以編寫並分析簡單的多線程程序。

進程與線程

進程:是一個正在執行的程序。
每一個進程執行都有執行順序,一個執行順序是一個執行路徑,或者叫控制單元;
每一個程序啓動時,都會在內存中分配一片空間,進程就用於標識這片空間,並封裝一個或若干控制單元。

線程:就是進程中的一個獨立的控制單元。
線程控制進程的執行,一個進程至少有一個線程。

java程序編譯時,java編譯器啓動,對應javac.exe進程啓動,編譯結束後javac.exe進程退出;java程序運行時,jvm啓動,對應java.exe進程啓動,java.exe中有一個主線程負責java程序的執行,這個主線程運行的代碼就存在於main方法中。其實jvm啓動時,不止一個主線程,還有負責垃圾回收機制的線程。

有多條執行路徑的程序,稱爲多線程程序。多線程的好處是可以讓程序的多個部分代碼產生同時運行的效果,程序的多個功能分支並行執行,優化程序功能結構並提高效率。

自定義創建線程的2種方法

1. 繼承Thread類具體步驟
    a) 自定義類,繼承Thread;
    b) 複寫Thread類的run()方法,run()方法中存儲線程要運行的代碼;
    c)  創建繼承Thread的自定義類對象,調用線程的start()方法,start()方法的作用:啓動線程,並自動調用run()方法。
2. 實現Runnable接口具體步驟
    a) 定義類實現Runnable接口;
    b) 覆蓋Runnable接口中的run()方法,run()中存放線程要運行的代碼;
    c) 創建Thread類線程對象,並將Runnable接口的子類對象作爲實參傳遞給Thread類構造函數;
    d) 調用Thread類對象的start()方法。
2種方式的區別:
    實現方式時,線程代碼存放在實現Runnable接口的子類的run()方法中,可以使用該子類創建多個Thread類,這樣多個線程運行時可以共用Runnable子類中的成員變量,實現資源的獨立共享。
    繼承方式時,線程代碼存放在Thread子類的run()方法中,而一個線程不能多次start(),所以達不到Thread子類中資源數據的共享使用。
    自定義線程時,建議使用實現Runnable接口的方式,因爲這樣還可以避免單繼承的侷限性。

線程的幾個零散知識點

多線程運行結果的隨機性:單核CPU環境,多個線程並非真正的同時運行,而是互相搶奪CPU的執行權限和資源,誰搶到誰執行,至於執行多長時間,CPU說了算(所以多線程程序的每次運行結果可能都不一樣)(後續可以加以控制)。
多核CPU環境,多個線程可以分佈運行到多個CPU上,實現真正的同時運行。
    多核CPU環境上多個線程同時打印輸出信息時,可能打印順序混亂,這是因爲多個CPU核搶佔DOS輸出屏是隨機的,有的打印被臨時阻塞。
   多核CPU時,程序運行效率就卡在了內在空間上,必須要有足夠大的內存存儲很多線程,才能讓這些線程運行在多個CPU上。
線程狀態及狀態間切換

已start()過的線程不能再次start(), 否則會報異常java.lang.IllegalThreadStateException。
線程的名稱
線程對象都有自己默認的名稱:Thread-編號,編號從0開始。
設置自定義線程名稱,可以在子類構造函數中調用super(name), 也可以直接創建對象後調用setName()方法。
Thread.currentThread(), 返回當前運行的線程對象,也就是this引用指向的對象。

多線程安全問題

當多條語句在操作多個線程共享數據時,一個線程對多條語句執行了一部分,還沒執行完,另一個線程參與進來執行,會導致共享數據的錯誤。
解決方法:對多條操作共享數據語句,只能讓一個線程執行完,在執行過程中,其他線程不可以參與執行。
java對多線程安全問題的專業解決方法就是同步synchronized,具體表現形式有同步代碼塊和同步函數。
同步代碼塊

synchronized(對象) //括號中對象需手動指定,可以直接在Object對象
{
    需要被同步的代碼
}
同步函數:將synchronized作爲修飾符放在函數定義上,函數返回值類型前面。
同步的原理
     同步代碼塊對象如同鎖,持有鎖的線程可以在同步語句中執行;沒有持有鎖的線程即使獲得了CPU執行權,也進不去,無法執行同步代碼。
     同步函數使用的鎖是this對象;靜態同步函數使用的鎖是該函數所在類對應的類字節碼文件對象,即類名.class,該對象的類型是Class。
     synchronized修飾符不屬於方法簽名的一部分,當子類覆蓋父類方法時,synchronized修飾符不會被繼承,因此接口中方法不能被聲明爲synchronized,同樣,構造函數也不能被聲明爲synchronized。
     線程進入同步代碼塊或同步函數前先判斷鎖標誌位,若判斷結果爲真,則進入同步代碼塊或同步函數後,修改鎖標誌位爲假,線程退出後,再恢復鎖標誌位爲真。
/*
簡單的售票程序,多個窗口同時賣票
*/
class Ticket implements Runnable{
    private int tick=100;
    Object obj=new Object();
    public void run(){
        while(true){
            synchronized(obj){ /*將操作共享成員數據tick的語句放到同步代碼塊中,用Object類對象鎖住,這樣不會出現賣0號票或負數票的情況*/
                if(tick>0){
                    try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();}
                    System.out.println(Thread.currentThread().getName()+"......sale : "+tick--);
                }
            }
        }
    }
}
public class TicketDemo{
    public static void main(String[] args){
        Ticket t=new Ticket();
        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        Thread t3=new Thread(t);
        Thread t4=new Thread(t);
        t1.start();
        t2.start();
        t3.start(); 
        t4.start();        
    }
}

同步的前提
1. 必須要有2個或者2個以上的線程
2. 必須是多個線程使用同一個鎖,多個線程可以同時操作同一個鎖下的代碼。
同步的弊端
1. 線程每次進入同步代碼塊或同步函數都要判斷鎖,浪費資源,影響效率
2. 可能出現死鎖現象,多發生在一個同步代碼塊或同步函數中嵌套另一個同步函數或同步代碼塊,且2個同步上使用不同的鎖。即同步中嵌套同步而鎖不同就容易引發死鎖。
下面是一個很直觀的死鎖的例子,跟畢老師講得MyLock的例子原理一樣,只是形式上有差別:

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() ;
        }
};
運行結果:
張三對李四說:“你給我畫,我就把書給你。”
李四對張三說:“你給我書,我就把畫給你”  
//雙方僵持在這,誰都沒法繼續運行

線程間通訊

Object類方法wait(),notify(),notifyAll()
      線程執行wait()後,就放棄了運行資格,處於凍結狀態;線程運行時,內存中會建立一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結的線程。
      notifyall(), 喚醒線程池中所有線程。
      wait(), notify(),notifyall()都用在同步裏面,因爲這3個函數是對持有鎖的線程進行操作,而只有同步纔有鎖,所以要使用在同步中。
      wait(),notify(),notifyall(),  在使用時必須標識它們所操作的線程持有的鎖,因爲等待和喚醒必須是同一鎖下的線程;而鎖可以是任意對象,所以這3個方法都是Object類中的方法。

wait和sleep區別:從執行權和鎖上來分析這2個方法
wait():可以指定時間也可以不指定時間,不指定時間時,只能由對應的notify()或notifyAll()來喚醒。
sleep():必須指定時間,時間到自動從凍結狀態轉入運行狀態或臨時阻塞狀態。
wait():線程會釋放執行權,並釋放鎖。
sleep():線程會釋放執行權,但是並不釋放鎖。

單個消費者生產者例子:

class Resource{  //生產者和消費者都要操作的資源
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name){
        if(flag)
            try{wait();}catch(Exception e){}
        this.name=name+"---"+count++;
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
        flag=true;
        this.notify();
    }
    public synchronized void out(){
        if(!flag)
            try{wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
        flag=false;
        this.notify();
    }
}
class Producer implements Runnable{
    private Resource res;
    Producer(Resource res){
        this.res=res;
    }
    public void run(){
        while(true){
            res.set("商品");
        }
    }
}
class Consumer implements Runnable{
    private Resource res;
    Consumer(Resource res){
        this.res=res;
    }
    public void run(){
        while(true){
            res.out();
        }
    }
}
public class ProducerConsumerDemo{
    public static void main(String[] args){
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        t1.start();
        t2.start();
    }
}//運行結果正常,生產者生產一個商品,緊接着消費者消費一個商品。

      但是如果有多個生產者和多個消費者,上面的代碼是有問題,比如2個生產者,2個消費者,運行結果就可能出現生產的1個商品生產了一次而被消費了2次,或者連續生產2個商品而只有1個被消費,這是因爲此時共有4個線程在操作Resource對象r,  而notify()喚醒的是線程池中第1個wait()的線程,所以生產者執行notify()時,喚醒的線程有可能是另1個生產者線程,這個生產者線程從wait()中醒來後不會再判斷flag,而是直接向下運行打印出一個新的商品,這樣就出現了連續生產2個商品。
爲了避免這種情況,修改代碼如下:

class Resource{
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name){
        while(flag) /*原先是if,現在改成while,這樣生產者線程從凍結狀態醒來時,還會再判斷flag.*/
            try{wait();}catch(Exception e){}
        this.name=name+"---"+count++;
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
        flag=true;
        this.notifyAll();/*原先是notity(), 現在改成notifyAll(),這樣生產者線程生產完一個商品後可以將等待中的消費者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/
    }
    public synchronized void out(){
        while(!flag) /*原先是if,現在改成while,這樣消費者線程從凍結狀態醒來時,還會再判斷flag.*/
            try{wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
        flag=false;
        this.notifyAll(); /*原先是notity(), 現在改成notifyAll(),這樣消費者線程消費完一個商品後可以將等待中的生產者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/
    }
}
public class ProducerConsumerDemo{
    public static void main(String[] args){
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        Thread t3=new Thread(pro);
        Thread t4=new Thread(con);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

jdk1.5中,提供了多線程的升級解決方案:將同步synchronized替換爲顯式的Lock操作,將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象可以通過Lock鎖對象獲取; 一個Lock對象上可以綁定多個Condition對象,這樣實現了本方線程只喚醒對方線程,而jdk1.5之前,一個同步只能有一個鎖,不同的同步只能用鎖來區分,且鎖嵌套時容易死鎖。

class Resource{
    private String name;
    private int count=1;
    private boolean flag=false;
    private Lock lock = new ReentrantLock();/*Lock是一個接口,ReentrantLock是該接口的一個直接子類。*/
    private Condition condition_pro=lock.newCondition(); /*創建代表生產者方面的Condition對象*/
    private Condition condition_con=lock.newCondition(); /*使用同一個鎖,創建代表消費者方面的Condition對象*/
    
    public void set(String name){
        lock.lock();//鎖住此語句與lock.unlock()之間的代碼
        try{
            while(flag)
                condition_pro.await(); //生產者線程在conndition_pro對象上等待
            this.name=name+"---"+count++;
            System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
            flag=true;
             condition_con.signalAll();/*signalAll()是喚醒線程池中的所有線程,而指明調用對象是condition_con後是喚醒所有在condition_conn這個對象上等待的所有線程*/
        }
        finally{
            lock.unlock(); //unlock()要放在finally塊中。
        }
    }
    public void out(){
        lock.lock(); //鎖住此語句與lock.unlock()之間的代碼
        try{
            while(!flag)
                condition_con.await(); //消費者線程在conndition_con對象上等待
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
        flag=false;
        condition_pro.signqlAll(); /*喚醒所有在condition_pro對象下等待的線程,也就是喚醒所有生產者線程*/
        }
        finally{
            lock.unlock();
        }
    }
}

線程通信的其他幾個常用方法:

終止線程
jdk1.5起,stop()方法(非靜態)已過時,不能再使用(否則會報錯),終止線程的唯一方法是run()方法結束。
開啓多線程運行時,運行代碼通過是循環結構,只要控制住循環,就可以讓run()方法結束。
中斷線程
interrupt()方法,如果線程在調用Object類的 wait()、wait(long) 或wait(long,int) 方法,或者該類的 join()、join(long)、join(long,int)、sleep(long) 或sleep(long,int) 方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException。  
線程的中斷狀態即凍結狀態,interrupt()是將處於凍結狀態的線程強制地恢復到運行狀態。  
守護線程
setDaemon(), 將線程設置爲守護線程,當正在運行的所有線程都是守護線程時,jvm自動退出。意思差不多是:前臺線程(如main線程)結束後,後臺線程(如t1,t2)也自動結束。
setDaemon()方法必須在啓動線程前調用。下面是interrupt()和setDeamon()方法的一個示例。

class StopThread implements Runnable{
	private boolean flag=true;
	public synchronized void run(){
		while(flag){
			try{
				wait(); /*t1或t2線程處於wait()凍結狀態時,即使主線程中修改了flag的值,t1和t2都 不能再判斷上面while循環的終止條件,會導致2個線程一直在wait()中動不了,所以需要將t1和t2人爲的喚醒*/
			}
			catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"...InterruptedException");
				flag=false;//一時接收到了InterruptedException異常,說明線程已被恢復到運行狀態,這時再手動設置flag爲false,讓線程再次判斷while循環時不再再次等待*/
			}
			System.out.println(Thread.currentThread().getName()+"...run");
		}
	}
	public void changeFlag(){
		flag=false;
	}
}
public class StopTreadDemo {
	public static void main(String[] args) {
		StopThread st=new StopThread();
		Thread t1=new Thread(st);
		Thread t2=new Thread(st);
		//t1.setDaemon(true);這2句語句執行後,t1,t2不再調用interrupt(),也能讓整個程序結束,因爲該程序就3個線程,main線程結束後守護線程也會隨之終止。
		//t2.setDaemon(true);
		t1.start();
		t2.start();
		int num=0;
		while(true){
			if(num++==60){
				//st.changFlag();
				t1.interrupt(); //將線程t1的凍結狀態 清除,讓其處於運行
				t2.interrupt(); //將線程t1的凍結狀態 清除,讓其處於運行
				break;
			}
			System.out.println(Thread.currentThread().getName()+"..."+num);
		}
		System.out.println("over");
	}

}

join()方法
當A線程執行到了B線程的join()方法時,A就放棄運行資格,處於凍結等待狀態,等B線程執行完,A才恢復運行資格;如果B線程執行過程中掛掉,那需要用interrupt()方法來清理A線程的凍結狀態;join()可以用來臨時加入線程執行。
toString()方法
返回線程名稱、優先級和線程組字符串。
默認情況下,哪個線程啓動了線程t1, t1就屬於哪個線程組,也可創建新的ThreadGroup對象;所有方法,包括main(),線程優先級默認是5;Thread.MAX_PRORITY爲10,Thread.MIN_PROTITY爲1,NOR_PRORITY爲5.
yield()方法
暫時釋放執行資格,稍微減緩線程切換的頻率,讓多個線程得到運行資格的機會均等一些。

  

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章