一、概述:
1、線程是什麼呢?
我們先來說一說比較熟悉的進程吧,之後就比較容易理解線程了。所謂進程,就是一個正在執行(進行)中的程序。每一個進程的執行都有一個執行順序,或者說是一個控制單元。簡單來說,就是你做一件事所要進行的一套流程。線程,就是進程中的一個獨立的控制單元;也就是說,線程是愛控制着進程的執行。一個進程至少有一個線程,並且線程的出現使得程序要有效率。打個比方說,在倉庫搬運貨物,一個人搬運和五個人搬運效率是不一樣的,搬運貨物的整個程序,就是進程;每一個人搬運貨物的過程,就是線程。
2、java中的線程:
在java中,JVM虛擬機啓動時,會有一個進程爲java.exe,該程序中至少有一個線程負責java程序的執行;而且該程序運行的代碼存在於main方法中,該線程稱之爲主線程。其實,JVM啓動時不止有一個線程(主線程),由於java是具有垃圾回收機制的,所以,在進程中,還有負責垃圾回收機制的線程。
3、多線程的意義:
透過上面的例子,可以看出,多線程有兩方面的意義:
1)提高效率。 2)清除垃圾,解決內存不足的問題。
二、自定義線程:
線程有如此的好處,那要如何才能通過代碼自定義一個線程呢?其實,線程是通過系統創建和分配的,java是不能獨立創建線程的;但是,java是可以通過調用系統,來實現對進程的創建和分配的。java作爲一種面向對象的編程語言,是可以將任何事物描述爲對象,從而進行操作的,進程也不例外。我們通過查閱API文檔,知道java提供了對線程這類事物的描述,即Thread類。創建新執行線程有兩種方法:
一)創建線程方式一:繼承Thread類。
1、步驟:
第一、定義類繼承Thread。
第二、複寫Thread類中的run方法。
第三、調用線程的start方法。分配並啓動該子類的實例。
start方法的作用:啓動線程,並調用run方法。
- <span style="font-family: Arial; ">class Demo extends Thread
- {
- public void run()
- {
- for (int i=0;i<60;i++)
- System.out.println(Thread.currentThread().getName() + "demo run---" + i);
- }
- }
- class Test2
- {
- public static void main(String[] args)
- {
- Demo d1 = new Demo();//創建一個對象就創建好了一個線程
- Demo d2 = new Demo();
- d1.start();//開啓線程並執行run方法
- d2.start();
- for (int i=0;i<60;i++)
- System.out.println("Hello World!---" + i);
- }
- }</span>
2、運行特點:
A.併發性:我們看到的程序(或線程)併發執行,其實是一種假象。有一點需要明確:;在某一時刻,只有一個程序在運行(多核除外),此時cpu是在進行快速的切換,以達到看上去是同時運行的效果。由於切換時間是非常短的,所以我們可以認爲是在併發進行。
B.隨機性:在運行時,每次的結果不同。由於多個線程都在獲取cpu的執行權,cpu執行到哪個線程,哪個線程就會執行。可以將多線程運行的行爲形象的稱爲互相搶奪cpu的執行權。這就是多線程的特點,隨機性。執行到哪個程序並不確定。
3、覆蓋run方法的原因:
1)Thread類用於描述線程。該類定義了一個功能:用於存儲線程要運行的代碼,該存儲功能即爲run方法。也就是說,Thread類中的run方法用於存儲線程要運行的代碼,就如同main方法存放的代碼一樣。
2)複寫run的目的:將自定義代碼存儲在run方法中,讓線程運行要執行的代碼。直接調用run,就是對象在調用方法。調用start(),開啓線程並執行該線程的run方法。如果直接調用run方法,只是將線程創建了,但未運行。
二)創建線程方式二:實現Runnable接口
1、步驟:
第一、定義類實現Runnable接口。
第二、覆蓋Runnable接口中的run方法。
第三、通過Thread類建立線程對象。要運行幾個線程,就創建幾個對象。
第四、將Runnable接口的子類對象作爲參數傳遞給Thread類的構造函數。
第五、調用Thread類的start方法開啓線程,並調用Runnable接口子類的run方法。
- <span style="font-family: Arial; ">
- //多個窗口同時賣票
- class Ticket implements Runnable
- {
- private int tic = 20;
- public void run()
- {
- while(true)
- {
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
- }
- }
- }
- 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();
- }
- }
- </span>
2、說明:
A.步驟2覆蓋run方法:將線程要運行的代碼存放在該run方法中。
B.步驟4:爲何將Runnable接口的子類對象傳給Thread構造函數。因爲自定義的run方法所屬對象爲Runnable接口的子類對象,所以讓線程指定對象的run方法,就必須明確該run方法所屬的對象。
三)實現方式與繼承方式有何區別:
1、實現方式:避免了單繼承的侷限性。
在定義線程時,建議使用實現方式。
2區別:
繼承Thread:線程代碼存放在Thread子類的run方法中。
實現Runnable:線程代碼存在接口的子類run方法中。
需要注意的是:局部變量在每一個線程中都獨有一份。
四)Thread類中的一些方法簡介:
1、獲取線程名稱:getName()
每個線程都有自己默認的名稱,獲取格式:對象.getName();打印後,顯示爲:Thread-編號(從0開始),也就是說,線程一爲:Thread-0,線程二爲:Thread-1。也可以獲取當前線程對象的名稱,通過currentThread().getName()。如上面方式二的結果爲
2、設置線程名稱:setName()或構造函數
可以通過setName()設置線程名稱,或者通過含有參數的構造函數直接顯式初始化線程的名稱,如Test(String name)。
三、線程的運行狀態
線程運行狀態可用如下圖示說明:
需要說明的是:
A.阻塞狀態:具備運行資格,但是沒有執行權,必須等到cpu的執行權,才轉到運行狀態,。
B.凍結狀態:放棄了cpu的執行資格,cpu不會將執行權分配給這個狀態下的線程,必須被喚醒後,此線程要先轉換到阻塞狀態,等待cpu的執行權後,纔有機會被執行到。
四、多線程的安全問題:
在那個簡單的賣票小程序中,發現打印出了0、-1、-2等錯票,也就是說這樣的多線程在運行的時候是存在一定的安全問題的。
爲什麼會出現這種安全問題呢?
原因是當多條語句在操作同一線程共享數據時,一個線程對多條語句只執行了一部分,還未執行完,另一線程就參與進來執行了,導致共享數據發生錯誤。以也就是說,由於cpu的快速切換,當執行線程一時,tic爲1了,執行到if (tic > 0)的時候,cpu就可能將執行權給了線程二,那麼線程一就停在這條語句了,tic還沒減1,仍爲1;線程二也判斷if (tic > 0)是符合的,也停在這,以此類推。當cpu再次執行線程一的時候,打印的是1號,執行線程二的時候,是2號票,以此類推,就出現了錯票的結果。其實就是多條語句被共享了,如果是一條語句,是不會出現此種情況的。
那麼該如何解決呢?
對於多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可參與執行,就不會出現問題了。Java對於多線程的安全問題,提供了專業的解決方式,即同步代碼塊,可操作共享數據。
1、同步代碼塊
格式:
- <span style="font-family: Arial; ">
- synchronized(對象)//對象稱爲鎖旗標
- {
- 需要被同步的代碼
- }</span><span style="font-family: SimSun; font-size: 14px; ">
- </span>
1)同步的前提:
A.必須有兩個或兩個以上的線程
B.必須保證同步的線程使用同一個鎖。必須保證同步中只能有一個線程在運行。
好處與弊端:解決了多線程的安全問題。多個線程需要判斷鎖,較爲消耗資源。
示例:
- <span style="font-family: Arial; ">class Ticket implements Runnable
- {
- private int tic = 100;
- Object obj = new Object();
- public void run()
- {
- while(true)
- {
- synchronized(obj)//任意的一個對象
- {
- //此兩句爲共享語句
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
- }
- }
- }
- }
- class TicketDemo
- {
- public static void main(String[] args)
- {
- Ticket t = new Ticket();
- Thread t1 = new Thread(t,"1");//創建第一個線程
- Thread t2 = new Thread(t,"2");//創建第二個線程
- //開啓線程
- t1.start();
- t2.start();
- }
- }
- </span>
2、同步函數
同步函數就是將修飾符synchronized放在返回類型的前面,下面通過同步函數給出多線程安全問題的具體解決方案:
1)目的:判斷程序中是否有安全問題,若有,該如何解決。
2)解決:第一、明確哪些代碼是多線程的運行代碼
第二、明確共享數據
第三、明確多線程運行代碼中,哪些語句是操作共享數據的。
示例:
- <span style="font-family: Arial; ">class Bank
- {
- private int sum;//共享數據
- //run中調用了add,所以其也爲多線程運行代碼
- public synchronized void add(int n)//同步函數,用synchronized修飾
- {
- //這有兩句操作,是操作共享數據的
- sum += n;
- System.out.println("sum" + sum);
- }
- }
- class Cus implements Runnable
- {
- private Bank b = new Bank();//共享數據
- //多線程運行代碼run
- public void run()
- {
- for (int i=0;i<3;i++)
- {
- b.add(100);//一句,不會分開執行,所以沒問題
- }
- }
- }
- class BankDemo
- {
- public static void main(String[] args)
- {
- Cus c = new Cus();
- Thread t1 = new Thread(c);
- Thread t2 = new Thread(c);
- t1.start();
- t2.start();
- }
- }
- </span>
五、同步函數中的鎖:
1、非靜態同步函數中的鎖---> this
函數需被對象調用,那麼函數都有一個所屬的對象引用,就是this,因此同步函數使用的鎖爲this。測驗如下:
- <span style="font-family: Arial; ">
- class Ticket implements Runnable
- {
- private int tic = 100;
- boolean flog = true;
- public void run()
- {
- if (flog)
- {
- //線程一執行
- while(true)
- {
- //如果對象爲obj,則是兩個鎖,是不安全的;換成this,爲一個鎖,會安全很多
- synchronized(this)
- {
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);
- }
- }
- }
- //線程二執行
- else
- while(true)
- show();
- }
- public synchronized void show()
- {
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
- }
- }
- class ThisLockDemo
- {
- public static void main(String[] args)
- {
- Ticket t = new Ticket();
- Thread t1 = new Thread(t);//創建一個線程
- Thread t2 = new Thread(t);//創建一個線程
- t1.start();
- t.flog = false;//開啓線程一,即關閉if,讓線程二執行else中語句
- t2.start();
- }
- }</span>
讓線程一執行打印cobe的語句,讓線程二執行打印show的語句。如果對象換位另一個對象obj,那將是兩個鎖,因爲在主函數中創建了一個對象即Ticket t = new Ticket();,線程會共享這個對象調用的run方法中的數據,所以都是這個t對象在調用,那麼,其中的對象應爲this;否則就破壞了同步的前提,就會出現安全問題。
2、靜態同步函數中的鎖:
如果同步函數被靜態修飾後,經驗證,使用的鎖不是this了,因爲靜態方法中不可定義this,所以,這個鎖不再是this了。靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象:類名.class;該對象的類型是Class。
所以靜態的同步方法使用的鎖是該方法所在類的字節碼文件對象,即類名.class。
示例:
- <span style="font-family: Arial; ">class Ticket implements Runnable
- {
- //私有變量,共享數據
- private static int tic = 100;
- boolean flog = true;
- public void run()
- {
- //線程一執行
- if (flog)
- {
- while(true)
- {
- synchronized(Ticket.class)//不再是this了,是Ticket.class
- {
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);
- }
- }
- }
- //線程二執行
- else
- while(true)
- show();
- }
- public static synchronized void show()
- {
- if (tic > 0)
- System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
- }
- }
- class StaticLockDemo
- {
- public static void main(String[] args)
- {
- Ticket t = new Ticket();
- Thread t1 = new Thread(t);//創建第一個線程
- Thread t2 = new Thread(t);//創建第二個線程
- t1.start();
- t.flog = false;
- t2.start();
- }
- }
- </span>
在之前,也提到過關於多線程的安全問題的相關知識,就是在單例設計模式中的懶漢式中,用到了鎖的機制。
具體請看單例設計模式中的內容:http://blog.csdn.net/shengfeixiang/article/details/8592722
六、多線程間的通信:
多線程間通信是線程之間進行交互的方式,簡單說就是存儲資源和獲取資源。比如說倉庫中的貨物,有進貨的,有出貨的。還比如生產者和消費者的例子。這些都可以作爲線程通信的實例。那麼如何更好地實現通信呢?先看下面的代碼:
- /*
- 線程間通信:
- 等待喚醒機制:升級版
- 生產者消費者 多個
- */
- import java.util.concurrent.locks.*;
- class ProducerConsumerDemo{
- public static void main(String[] args){
- Resouse r = new Resouse();
- Producer p = new Producer(r);
- Consumer c = new Consumer(r);
- Thread t1 = new Thread(p);
- Thread t2 = new Thread(c);
- Thread t3 = new Thread(p);
- Thread t4 = new Thread(c);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- }
- }
- class Resouse{
- private String name;
- private int count = 1;
- private boolean flag = false;
- private Lock lock = new ReentrantLock();
- private Condition condition_P = lock.newCondition();
- private Condition condition_C = lock.newCondition();
- //要喚醒全部,否則都可能處於凍結狀態,那麼程序就會停止。這和死鎖有區別的。
- public void set(String name)throws InterruptedException{
- lock.lock();
- try{
- while(flag)//循環判斷,防止都凍結狀態
- condition_P.await();
- this.name = name + "--" + count++;
- System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);
- flag = true;
- condition_C.signal();
- }finally{
- lock.unlock();//釋放鎖的機制一定要執行
- }
- }
- public void out()throws InterruptedException{
- lock.lock();
- try{
- while(!flag)//循環判斷,防止都凍結狀態
- condition_C.await();
- System.out.println(Thread.currentThread().getName() + "..消費者." + this.name);
- flag = false;
- condition_P.signal();//喚醒全部
- }finally{
- lock.unlock();
- }
- }
- }
- class Producer implements Runnable{
- private Resouse r;
- Producer(Resouse r){
- this.r = r;
- }
- public void run(){
- while(true){
- try{
- r.set("--商品--");
- }catch (InterruptedException e){}
- }
- }
- }
- class Consumer implements Runnable{
- private Resouse r;
- Consumer(Resouse r){
- this.r = r;
- }
- public void run(){
- while(true){
- try{
- r.out();
- }catch (InterruptedException e){}
- }
- }
- }
一)等待喚醒機制:
1、顯式鎖機制和等待喚醒機制:
在JDK 1.5中,提供了改進synchronized的升級解決方案。將同步synchronized替換爲顯式的Lock操作,將Object中的wait,notify,notifyAll替換成Condition對象,該對象可對Lock鎖進行獲取。這就實現了本方喚醒對方的操作。在這裏說明幾點:
1)、對於wait,notify和notifyAll這些方法都是用在同步中,也就是等待喚醒機制,這是因爲要對持有監視器(鎖)的線程操作。所以要使用在同步中,因爲只有同步才具有鎖。
2)、而這些方法都定義在Object中,是因爲這些方法操作同步中的線程時,都必須表示自己所操作的線程的鎖,就是說,等待和喚醒的必須是同一把鎖。不可對不同鎖中的線程進行喚醒。所以這就使得程序是不良的,因此,通過對鎖機制的改良,使得程序得到優化。
3)、等待喚醒機制中,等待的線程處於凍結狀態,是被放在線程池中,線程池中的線程已經放棄了執行資格,需要被喚醒後,纔有被執行的資格。
2、對於上面的程序,有兩點要說明:
1)、爲何定義while判斷標記:
原因是讓被喚醒的線程再判斷一次。
避免未經判斷,線程不知是否應該執行,就執行本方的上一個已經執行的語句。如果用if,消費者在等着,兩個生成着一起判斷完flag後,cpu切換到其中一個如t1,另一個t3在wait,當t1喚醒凍結中的一個,是t3(因爲它先被凍結的,就會先被喚醒),所以t3未經判斷,又生產了一個。而沒消費。
2)這裏使用的是signal方法,而不是signalAll方法。是因爲通過Condition的兩個對象,分別喚醒對方,這就體現了Lock鎖機制的靈活性。可以通過Contidition對象調用Lock接口中的方法,就可以保證多線程間通信的流暢性了。
二)Thread類中的方法簡介:
在這簡單介紹幾個Thread中的方法:
1、停止線程:
在java 1.5之後,就不再使用stop方法停止線程了。那麼該如何停止線程呢?只有一種方法,就是讓run方法結束。
開啓多線程運行,運行代碼通常爲循環結構,只要控制住循環,就可以讓run方法結束,也就可以使線程結束。
注:
特殊情況:當線程處於凍結狀態,就不會讀取標記,那麼線程就不會結束。如下:
- <span style="font-family: Arial; ">class StopThread implements Runnable{
- private boolean flag = true;
- public synchronized void run(){
- while (flag){
- try{
- wait();
- }catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName() + "----Exception");
- flag = false;
- }
- 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 n = 0;
- while (true){
- if (n++ == 60){
- st.changeFlag();
- break;
- }
- System.out.println("Hello World!");
- }
- }
- }</span>
這時,當沒有指定的方式讓凍結的線程回覆打破運行狀態時,就需要對凍結進行清除。強制讓線程回覆到運行狀態來,這樣就可以操作標記讓線程結束。
在Thread類中提供了此種方法:interrupt()。此方法是爲了讓線程中斷,但是並沒有結束運行,讓線程恢復到運行狀態,再判斷標記從而停止循環,run方法結束,線程結束。
- <span style="font-family: Arial; ">class StopThread implements Runnable{
- private boolean flag = true;
- public synchronized void run(){
- while (flag){
- try{
- wait();
- }catch (InterruptedException e){
- System.out.println(Thread.currentThread().getName() + "----Exception");
- flag = false;
- }
- System.out.println(Thread.currentThread().getName() + "----run");
- }
- }
- }
- 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 n = 0;
- while (true){
- if (n++ == 60){
- t1.interrupt();
- t2.interrupt();
- break;
- }
- System.out.println("Hello World!");
- }
- }
- }
- </span>
2、守護線程:---setDaemon()
可將一個線程標記爲守護線程,直接調用這個方法。此方法需要在啓動前調用守護線程在這個線程結束後,會自動結束,則Jvm虛擬機也結束運行。
- <span style="font-family: Arial; "> ........
- //守護線程(後臺線程),在啓動前調用。後臺線程自動結束
- t1.setDaemon(true);
- t2.setDaemon(true);
- t1.start();
- t2.start();
- .........</span>
3、臨時加入線程:--join()
特點:當A線程執行到B線程方法時,A線程就會等待,B線程都執行完,A纔會執行。join可用來臨時加入線程執行。
- <span style="font-family: Arial; ">class Demo implements Runnable{
- public void run(){
- for(int x=0;x<90;x++){
- System.out.println(Thread.currentThread().getName() + "----run" + x);
- }
- }
- }
- class JoinDemo{
- public static void main(String[] args)throws Exception{
- Demo d = new Demo();
- Thread t1 = new Thread(d);
- Thread t2 = new Thread(d);
- t1.start();
- t2.start();
- t1.join();//等t1執行完了,主線程才從凍結狀態恢復,和t2搶執行權。t2執不執行完都無所謂。
- int n = 0;
- for(int x=0;x<80;x++){
- System.out.println(Thread.currentThread().getName() + "----main" + x);
- }
- System.out.println("Over");
- }
- }
- </span>
4、優先級:
setPriority():
在Thread中,存在着1~10這十個執行級別,最高的是 MAX_PRIORITY 爲10,最低是 MIN_PRIORITY 爲1,默認優先級是 NORM_PRIORITY 爲5;但是並不是優先級越高,就會一直執行這個線程,只是說會優先執行到這個線程,此後還是有其他線程會和此線程搶奪cpu執行權的。
優先級是可以設定的,可通過setPriority()設定,如:setPriority(Thread.MAX_PRIORITY)設優先級爲最大。
yield():
此方法可暫停當前線程,而執行其他線程。通過這個方法,可稍微減少線程執行頻率,達到線程都有機會平均被執行的效果。如下:
- <span style="font-family: Arial; ">class Demo implements Runnable{
- public void run(){
- for(int x=0;x<90;x++){
- System.out.println(Thread.currentThread().toString() + "----run" + x);
- Thread.yield();//稍微減少線程執行頻率。可達到線程都有機會達到平均運行的效果
- }
- }
- }
- class YieldDemo{
- public static void main(String[] args)throws Exception{
- Demo d = new Demo();
- Thread t1 = new Thread(d);
- Thread t2 = new Thread(d);
- t1.start();
- t1.setPriority(Thread.MAX_PRIORITY);//設置線程優先級最大
- t2.start();
- System.out.println("Over");
- }
- }</span>
對於多線程的知識,還需要慢慢積累,畢竟線程通信可以提高程序運行的效率,這樣就可以讓程序得到很大的優化。期待新知識······