-------
android培訓、java培訓、期待與您交流! ----------
一、多線程概述
要理解多線程,就必須理解線程。而要理解線程,就必須知道進程。
1、 進程
是一個正在執行的程序。
每一個進程執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元。
2、線程
就是進程中的一個獨立的控制單元。線程在控制着進程的執行。只要進程中有一個線程在執行,進程就不會結束。
一個進程中至少有一個線程。
3、多線程
在java虛擬機啓動的時候會有一個java.exe的執行程序,也就是一個進程。該進程中至少有一個線程負責java程序的執行。而且這個線程運行的代碼存在於main方法中。該線程稱之爲主線程。JVM啓動除了執行一個主線程,還有負責垃圾回收機制的線程。像種在一個進程中有多個線程執行的方式,就叫做多線程。
4、多線程存在的意義
多線程的出現能讓程序產生同時運行效果。可以提高程序執行效率。
例如:在java.exe進程執行主線程時,如果程序代碼特別多,在堆內存中產生了很多對象,而同時對象調用完後,就成了垃圾。如果垃圾過多就有可能是堆內存出現內存不足的現象,只是如果只有一個線程工作的話,程序的執行將會很低效。而如果有另一個線程幫助處理的話,如垃圾回收機制線程來幫助回收垃圾的話,程序的運行將變得更有效率。
5、計算機CPU的運行原理
我們電腦上有很多的程序在同時進行,就好像cpu在同時處理這所以程序一樣。但是,在一個時刻,單核的cpu只能運行一個程序。而我們看到的同時運行效果,只是cpu在多個進程間做着快速切換動作。
而cpu執行哪個程序,是毫無規律性的。這也是多線程的一個特性:隨機性。哪個線程被cpu執行,或者說搶到了cpu的執行權,哪個線程就執行。而cpu不會只執行一個,當執行一個一會後,又會去執行另一個,或者說另一個搶走了cpu的執行權。至於究竟是怎麼樣執行的,只能由cpu決定。
二、創建線程的方式
創建線程共有兩種方式:繼承方式和實現方式(簡單的說)。
1、 繼承方式
通過查找java的幫助文檔API,我們發現java中已經提供了對線程這類事物的描述的類——Thread類。這第一種方式就是通過繼承Thread類,然後複寫其run方法的方式來創建線程。
創建步驟:
a,定義類繼承Thread。
b,複寫Thread中的run方法。
目的:將自定義代碼存儲在run方法中,讓線程運行。
c,創建定義類的實例對象。相當於創建一個線程。
d,用該對象調用線程的start方法。該方法的作用是:啓動線程,調用run方法。
注:如果對象直接調用run方法,等同於只有一個線程在執行,自定義的線程並沒有啓動。
覆蓋run方法的原因:
Thread類用於描述線程。該類就定義了一個功能,用於存儲線程要執行的代碼。該存儲功能就run方法。也就是說,Thread類中的run方法,用於存儲線程要運行的代碼。
程序示例:
- /*
- 小練習
- 創建兩線程,和主線程交替運行。
- */
- //創建線程Test
- class Test extends Thread
- {
- // private String name;
- Test(String name)
- {
- super(name);
- // this.name=name;
- }
- //複寫run方法
- public void run()
- {
- for(int x=0;x<60;x++)
- System.out.println(Thread.currentThread().getName()+"..run..."+x);
- // System.out.println(this.getName()+"..run..."+x);
- }
- }
- class ThreadTest
- {
- public static void main(String[] args)
- {
- new Test("one+++").start();//開啓一個線程
- new Test("tow———").start();//開啓第二線程
- //主線程執行的代碼
- for(int x=0;x<170;x++)
- System.out.println("Hello World!");
- }
- }
結果:
如圖,執行是隨機、交替執行的,每一次運行的結果都會不同。
2、 實現方式
使用繼承方式有一個弊端,那就是如果該類本來就繼承了其他父類,那麼就無法通過Thread類來創建線程了。這樣就有了第二種創建線程的方式:實現Runnable接口,並複習其中run方法的方式。
創建步驟:
a,定義類實現Runnable的接口。
b,覆蓋Runnable接口中的run方法。目的也是爲了將線程要運行的代碼存放在該run方法中。
c,通過Thread類創建線程對象。
d,將Runnable接口的子類對象作爲實參傳遞給Thread類的構造方法。
爲什麼要將Runnable接口的子類對象傳遞給Thread的構造函數?
因爲,自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。
e,調用Thread類中start方法啓動線程。start方法會自動調用Runnable接口子類的run方法。
實現方式好處:避免了單繼承的侷限性。在定義線程時,建議使用實現方式。
程序示例:
- /*
- 需求:簡單的賣票程序。
- 多個窗口賣票。
- */
- class Ticket implements Runnable//extends Thread
- {
- private int tick = 100;
- public void run()
- {
- while(true)
- {
- if(tick>0)
- {
- //顯示線程名及餘票數
- System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
- }
- }
- }
- }
- class TicketDemo
- {
- public static void main(String[] args)
- {
- //創建Runnable接口子類的實例對象
- 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、兩種創建方式的區別
繼承Thread:線程代碼存放在Thread子類run方法中。
實現Runnable:線程代碼存放在接口子類run方法中。
2、幾種狀態
被創建:等待啓動,調用start啓動。
運行狀態:具有執行資格和執行權。
臨時狀態(阻塞):有執行資格,但是沒有執行權。
凍結狀態:遇到sleep(time)方法和wait()方法時,失去執行資格和執行權,sleep方法時間到或者調用notify()方法時,獲得執行資格,變爲臨時狀態。
消忙狀態:stop()方法,或者run方法結束。
注:當已經從創建狀態到了運行狀態,再次調用start()方法時,就失去意義了,java運行時會提示線程狀態異常。
圖解:
四、線程安全問題
1、導致安全問題的出現的原因:
當多條語句在操作同一線程共享數據時,一個線程對多條語句只執行了一部分,還沒用執行完,另一個線程參與進來執行。導致共享數據的錯誤。
簡單的說就兩點:
a、多個線程訪問出現延遲。
b、線程隨機性 。
注:線程安全問題在理想狀態下,不容易出現,但一旦出現對軟件的影響是非常大的。
2、解決辦法——同步
對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。
在java中對於多線程的安全問題提供了專業的解決方式——synchronized(同步)
這裏也有兩種解決方式,一種是同步代碼塊,還有就是同步函數。都是利用關鍵字synchronized來實現。
a、同步代碼塊
用法:
synchronized(對象)
{需要被同步的代碼}
同步可以解決安全問題的根本原因就在那個對象上。其中對象如同鎖。持有鎖的線程可以在同步中執行。沒有持有鎖的線程即使獲取cpu的執行權,也進不去,因爲沒有獲取鎖。
示例:
- /*
- 給賣票程序示例加上同步代碼塊。
- */
- class Ticket implements Runnable
- {
- private int tick=100;
- Object obj = new Object();
- public void run()
- {
- while(true)
- {
- //給程序加同步,即鎖
- synchronized(obj)
- {
- if(tick>0)
- {
- try
- {
- //使用線程中的sleep方法,模擬線程出現的安全問題
- //因爲sleep方法有異常聲明,所以這裏要對其進行處理
- Thread.sleep(10);
- }
- catch (Exception e)
- {
- }
- //顯示線程名及餘票數
- System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
- }
- }
- }
- }
- }
b,同步函數
格式:
在函數上加上synchronized修飾符即可。
那麼同步函數用的是哪一個鎖呢?
函數需要被對象調用。那麼函數都有一個所屬對象引用。就是this。所以同步函數使用的鎖是this。
拿同步代碼塊的示例:
- class Ticket implements Runnable
- {
- private int tick=100;
- Object obj = new Object();
- public void run()
- {
- while(true)
- {
- show();
- }
- }
- //直接在函數上用synchronized修飾即可實現同步
- public synchronized void show()
- {
- if(tick>0)
- {
- try
- {
- //使用線程中的sleep方法,模擬線程出現的安全問題
- //因爲sleep方法有異常聲明,所以這裏要對其進行處理
- Thread.sleep(10);
- }
- catch (Exception e)
- {
- }
- //顯示線程名及餘票數
- System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
- }
- }
- }
3、同步的前提
a,必須要有兩個或者兩個以上的線程。
b,必須是多個線程使用同一個鎖。
4、同步的利弊
好處:解決了多線程的安全問題。
弊端:多個線程需要判斷鎖,較爲消耗資源。
5、如何尋找多線程中的安全問題
a,明確哪些代碼是多線程運行代碼。
b,明確共享數據。
c,明確多線程運行代碼中哪些語句是操作共享數據的。
五、靜態函數的同步方式
如果同步函數被靜態修飾後,使用的鎖是什麼呢?
通過驗證,發現不在是this。因爲靜態方法中也不可以定義this。靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象。如:
類名.class 該對象的類型是Class
這就是靜態函數所使用的鎖。而靜態的同步方法,使用的鎖是該方法所在類的字節碼文件對象。類名.class
經典示例:
- /*
- 加同步的單例設計模式————懶漢式
- */
- class Single
- {
- private static Single s = null;
- private Single(){}
- public static void getInstance()
- {
- if(s==null)
- {
- synchronized(Single.class)
- {
- if(s==null)
- s = new Single();
- }
- }
- return s;
- }
- }
六、死鎖
當同步中嵌套同步時,就有可能出現死鎖現象。
示例:
- /*
- 寫一個死鎖程序
- */
- //定義一個類來實現Runnable,並複寫run方法
- class LockTest implements Runnable
- {
- private boolean flag;
- LockTest(boolean flag)
- {
- this.flag=flag;
- }
- public void run()
- {
- if(flag)
- {
- while(true)
- {
- synchronized(LockClass.locka)//a鎖
- {
- System.out.println(Thread.currentThread().getName()+"------if_locka");
- synchronized(LockClass.lockb)//b鎖
- {
- System.out.println(Thread.currentThread().getName()+"------if_lockb");
- }
- }
- }
- }
- else
- {
- while(true)
- {
- synchronized(LockClass.lockb)//b鎖
- {
- System.out.println(Thread.currentThread().getName()+"------else_lockb");
- synchronized(LockClass.locka)//a鎖
- {
- System.out.println(Thread.currentThread().getName()+"------else_locka");
- }
- }
- }
- }
- }
- }
- //定義兩個鎖
- class LockClass
- {
- static Object locka = new Object();
- static Object lockb = new Object();
- }
- class DeadLock
- {
- public static void main(String[] args)
- {
- //創建2個進程,並啓動
- new Thread(new LockTest(true)).start();
- new Thread(new LockTest(false)).start();
- }
- }
結果:程序卡住,不能繼續執行
七、線程間通信
其實就是多個線程在操作同一個資源,但是操作的動作不同。
1、使用同步操作同一資源的示例:
- /*
- 有一個資源
- 一個線程往裏存東西,如果裏邊沒有的話
- 一個線程往裏取東西,如果裏面有得話
- */
- //資源
- class Resource
- {
- private String name;
- private String sex;
- private boolean flag=false;
- public synchronized void setInput(String name,String sex)
- {
- if(flag)
- {
- try{wait();}catch(Exception e){}//如果有資源時,等待資源取出
- }
- this.name=name;
- this.sex=sex;
- flag=true;//表示有資源
- notify();//喚醒等待
- }
- public synchronized void getOutput()
- {
- if(!flag)
- {
- try{wait();}catch(Exception e){}//如果木有資源,等待存入資源
- }
- System.out.println("name="+name+"---sex="+sex);//這裏用打印表示取出
- flag=false;//資源已取出
- notify();//喚醒等待
- }
- }
- //存線程
- class Input implements Runnable
- {
- private Resource r;
- Input(Resource r)
- {
- this.r=r;
- }
- public void run()//複寫run方法
- {
- int x=0;
- while(true)
- {
- if(x==0)//交替打印張三和王羲之
- {
- r.setInput("張三",".....man");
- }
- else
- {
- r.setInput("王羲之","..woman");
- }
- x=(x+1)%2;//控制交替打印
- }
- }
- }
- //取線程
- class Output implements Runnable
- {
- private Resource r;
- Output(Resource r)
- {
- this.r=r;
- }
- public void run()//複寫run方法
- {
- while(true)
- {
- r.getOutput();
- }
- }
- }
- class ResourceDemo2
- {
- public static void main(String[] args)
- {
- Resource r = new Resource();//表示操作的是同一個資源
- new Thread(new Input(r)).start();//開啓存線程
- new Thread(new Output(r)).start();//開啓取線程
- }
- }
結果:部分截圖
幾個小問題:
1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?
a,這些方法存在與同步中。
b,使用這些方法時必須要標識所屬的同步的鎖。同一個鎖上wait的線程,只可以被同一個鎖上的notify喚醒。
c,鎖可以是任意對象,所以任意對象調用的方法一定定義Object類中。
2)wait(),sleep()有什麼區別?
wait():釋放cpu執行權,釋放鎖。
sleep():釋放cpu執行權,不釋放鎖。
3)爲甚麼要定義notifyAll?
因爲在需要喚醒對方線程時。如果只用notify,容易出現只喚醒本方線程的情況。導致程序中的所以線程都等待。
2、JDK1.5中提供了多線程升級解決方案。
將同步synchronized替換成顯示的Lock操作。將Object中wait,notify,notifyAll,替換成了Condition對象。該Condition對象可以通過Lock鎖進行獲取,並支持多個相關的Condition對象。
升級解決方案的示例:
- /*
- 生產者生產商品,供消費者使用
- 有兩個或者多個生產者,生產一次就等待消費一次
- 有兩個或者多個消費者,等待生產者生產一次就消費掉
- */
- import java.util.concurrent.locks.*;
- class Resource
- {
- private String name;
- private int count=1;
- private boolean flag = false;
- //多態
- private Lock lock=new ReentrantLock();
- //創建兩Condition對象,分別來控制等待或喚醒本方和對方線程
- Condition condition_pro=lock.newCondition();
- Condition condition_con=lock.newCondition();
- //p1、p2共享此方法
- public void setProducer(String name)throws InterruptedException
- {
- lock.lock();//鎖
- try
- {
- while(flag)//重複判斷標識,確認是否生產
- condition_pro.await();//本方等待
- this.name=name+"......"+count++;//生產
- System.out.println(Thread.currentThread().getName()+"...生產..."+this.name);//打印生產
- flag=true;//控制生產\消費標識
- condition_con.signal();//喚醒對方
- }
- finally
- {
- lock.unlock();//解鎖,這個動作一定執行
- }
- }
- //c1、c2共享此方法
- public void getConsumer()throws InterruptedException
- {
- lock.lock();
- try
- {
- while(!flag)//重複判斷標識,確認是否可以消費
- condition_con.await();
- System.out.println(Thread.currentThread().getName()+".消費."+this.name);//打印消費
- flag=false;//控制生產\消費標識
- condition_pro.signal();
- }
- finally
- {
- lock.unlock();
- }
- }
- }
- //生產者線程
- class Producer implements Runnable
- {
- private Resource res;
- Producer(Resource res)
- {
- this.res=res;
- }
- //複寫run方法
- public void run()
- {
- while(true)
- {
- try
- {
- res.setProducer("商品");
- }
- catch (InterruptedException e)
- {
- }
- }
- }
- }
- //消費者線程
- class Consumer implements Runnable
- {
- private Resource res;
- Consumer(Resource res)
- {
- this.res=res;
- }
- //複寫run
- public void run()
- {
- while(true)
- {
- try
- {
- res.getConsumer();
- }
- catch (InterruptedException e)
- {
- }
- }
- }
- }
- class ProducerConsumer
- {
- public static void main(String[] args)
- {
- Resource res=new Resource();
- new Thread(new Producer(res)).start();//第一個生產線程 p1
- new Thread(new Consumer(res)).start();//第一個消費線程 c1
- new Thread(new Producer(res)).start();//第二個生產線程 p2
- new Thread(new Consumer(res)).start();//第二個消費線程 c2
- }
- }
運行結果:部分截圖
八、停止線程
在JDK 1.5版本之前,有stop停止線程的方法,但升級之後,此方法已經過時。
那麼現在我們該如果停止線程呢?
只有一種辦法,那就是讓run方法結束。
1、開啓多線程運行,運行代碼通常是循環結構。只要控制住循環,就可以讓run方法結束,也就是線程結束。
如:run方法中有如下代碼,設置一個flag標記。
- public void run()
- {
- while(flag)
- {
- System.out.println(Thread.currentThread().getName()+"....run");
- }
- }
那麼只要在主函數或者其他線程中,在該線程執行一段時間後,將標記flag賦值false,該run方法就會結束,線程也就停止了。
2、上面的1方法可以解決一般情況,但是有一種特殊情況:就是當線程處於凍結狀態。就不會讀取到標記。那麼線程就不會結束。
當沒有指定的方式讓凍結的線程恢復到運行狀態時,這時需要對凍結進行清除。強制讓線程恢復到運行狀態中來。這樣就可以操作標記讓線程結束。Thread類提供該方法interrupt();
如:
- class StopThread implements Runnable
- {
- private boolean flag =true;
- public void run()
- {
- while(flag)
- {
- System.out.println(Thread.currentThread().getName()+"....run");
- }
- }
- public void changeFlag()
- {
- flag = false;
- }
- }
- class StopThreadDemo
- {
- public static void main(String[] args)
- {
- StopThread st = new StopThread();
- Thread t1 = new Thread(st);
- Thread t2 = new Thread(st);
- t1.start();
- t2.start();
- int num = 0;
- while(true)
- {
- if(num++ == 60)
- {
- t1.interrupt();//清除凍結狀態
- t2.interrupt();
- st.changeFlag();//改變循環標記
- break;
- }
- System.out.println(Thread.currentThread().getName()+"......."+num);
- }
- System.out.println("over");
- }
- }
結果:
九、什麼時候寫多線程?
當某些代碼需要同時被執行時,就用單獨的線程進行封裝。
示例:
- class ThreadTest
- {
- public static void main(String[] args)
- {
- //一條線程
- new Thread()
- {
- public void run()
- {
- for (int x=0;x<100 ;x++ )
- {
- System.out.println(Thread.currentThread().toString()+"....."+x);
- }
- }
- }.start();
- //又是一條線程
- Runnable r= new Runnable()
- {
- public void run()
- {
- for (int x=0;x<100 ;x++ )
- {
- System.out.println(Thread.currentThread().getName()+"....."+x);
- }
- }
- };
- new Thread(r).start();
- //可以看作主線程
- for (int x=0;x<1000 ;x++ )
- {
- System.out.println("Hello World!");
- }
- }
- }