一、多線程概述
要理解多線程,就必須理解線程。而要理解線程,就必須知道進程。
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方法,用於存儲線程要運行的代碼。
執行是隨機、交替執行的,每一次運行的結果都會不同。
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方法。
實現方式好處:避免了單繼承的侷限性。在定義線程時,建議使用實現方式。
程序示例:
三、兩種方式的區別和線程的幾種狀態
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 Ticket1 implements Runnable//extends Thread
{
private static int tick = 100;
Object obj = new Object();
//用來標誌是線程是進去同步代碼塊還是同步函數
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
if(tick>0)
{
try{Thread.sleep(10);}catch (Exception e){}
System.out.println(Thread.currentThread().getName()+"...code.."+tick--);
}
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
if(tick>0)
{
try{Thread.sleep(10);}catch (Exception e){}
System.out.println(Thread.currentThread().getName()+"...show.."+tick--);
}
}
}
class ThisLockDemo
{
public static void main(String[] args)
{
Ticket1 t = new Ticket1();
Thread t1 = new Thread(t); //創建一個線程;
Thread t2 = new Thread(t); //創建一個線程;
//兩個線程使用的不是同一個鎖,t1使用的是obj對象的鎖,t2使用的是this的
t1.start();
try{Thread.sleep(100);}catch (Exception e){}
t.flag = false;
t2.start();
}
}
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;
}
}
六、死鎖
當同步中嵌套同步時,就有可能出現死鎖現象。
示例:
七、線程間通信
其實就是多個線程在操作同一個資源,但是操作的動作不同。
代碼示例:
/*
生產者消費者問題
一個生產者
一個消費者
共享一個緩存區
*/
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(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
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();
}
}
}
/*
生產者消費者問題
多個生產者
多個消費者
共享一個緩存區
爲什麼要定義while判斷標記?
原因:讓被喚醒的線程再一次判斷標記。
爲什麼定義notifyAll?
因爲需要喚醒對方線程(要喚醒所有線程)
因爲只用notify,容易出現只喚醒本方線程的情況。導致程序中的所有線程都等待。
*/
class ProducerConsumerDemo2
{
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(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)
try{wait(); } catch(Exception e){}
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
flag = true;
this.notifyAll();
}
public synchronized void out()
{
while(!flag)
try{wait(); } catch(Exception e){} //等待線程
System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name);
flag = false;
this.notifyAll(); //喚醒線程池中的所有線程
}
}
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();
}
}
}
幾個小問題:
1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?
a,這些方法存在與同步中。
b,使用這些方法時必須要標識所屬的同步的鎖。同一個鎖上wait的線程,只可以被同一個鎖上的notify喚醒。
c,鎖可以是任意對象,所以任意對象調用的方法一定定義Object類中。
2)wait(),sleep()有什麼區別?
wait():釋放cpu執行權,釋放鎖。
sleep():釋放cpu執行權,不釋放鎖。
3)爲甚麼要定義notifyAll?
因爲在需要喚醒對方線程時。如果只用notify,容易出現只喚醒本方線程的情況。導致程序中的所以線程都等待。
2、JDK1.5中提供了多線程升級解決方案。
/*
生產者消費者問題JDK1.5升級版
JDK1.5中提供了多線程的升級解決方案。
將同步synchronized替換成顯示Lock操作。
將object中的wait,notify,notifyAll 替換成了condition對象。
該對象可以用Lock鎖進行獲取。
該示例中,實現了本方只喚醒對方操作。
*/
import java.util.concurrent.locks.*;
class ProducerConsumerDemo3
{
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(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock(); //定義一個顯示的鎖
private Condition con_pro = lock.newCondition();
private Condition con_con = lock.newCondition();
public void set(String name) throws InterruptedException
{
lock.lock();
try
{
while(flag)
con_pro.await(); //生產者等待
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
flag = true;
con_con.signal(); //喚醒消費者
}
finally
{
lock.unlock();
}
}
public void out() throws InterruptedException
{
lock.lock();
try
{
while(!flag)
con_con.await();//消費者線程等待
System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name);
flag = false;
con_pro.signal(); //喚醒生產者線程
}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.set("商品");
}
catch (InterruptedException e)
{
}
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.out();
}
catch (InterruptedException e)
{
}
}
}
}
八、停止線程
在JDK 1.5版本之前,有stop停止線程的方法,但升級之後,此方法已經過時。
/*
線程結束
stop方法已經過時了,如何停止線程呢?
只有一種方法,run方法結束。
開啓多線程運行,運行代碼通常是循環結構。
只要控制住循環,就可以讓run方法結束,也就是結束線程。
特殊情況:
當線程處於了凍結狀態時
就不會讀取到標記,那麼線程就不會結束。
當沒有指定的方式讓凍結的線程恢復到運行狀態時,這時就需要對凍結進行清除
強制讓線程恢復到運行狀態中來,這樣就可以操作標記讓線程結束。
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 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 num = 0;
while(true)
{
if(num++ == 100)
{
//st.changeFlag(); //設置標記,中斷線程
//t1.interrupt(); //線程中斷
//t2.interrupt(); //線程中斷
break;
}
System.out.println(Thread.currentThread().getName()+"..."+num);
}
}
}
擴展小知識:
1、join方法
當A線程執行到了b線程的.join()方法時,A線程就會等待,等B線程都執行完,A線程纔會執行。(此時B和其他線程交替運行。)join可以用來臨時加入線程執行。
2、setPriority()方法用來設置優先級
MAX_PRIORITY 最高優先級10
MIN_PRIORITY 最低優先級1
NORM_PRIORITY 分配給線程的默認優先級
3、yield()方法可以暫停當前線程,讓其他線程執行。