------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流!
一、線程
在計算機中,每一個程序的運行,都是通過線程來實現的。我們可以把一個正在執行的程序稱爲進程,每一個進程的執行都有一個執行順序。該順序是一個執行路徑,或者叫控制單元。線程就是進程中的一個獨立的控制單元,線程在控制着進程的執行。只要進程中有一個線程在執行,進程就不會結束。一個進程中至少有一個線程。
在多個程序運行時,CPU會隨機地在多個進程中快速切換,哪個線程搶到了CPU的執行權,哪個線程就運行。運行Java程序時,虛擬機啓動會有一個java.exe的進程,該進程中至少有一個線程負責java程序的執行。而且這個線程運行的代碼存在於main方法中,稱之爲主線程。虛擬機啓動除了執行主線程外,還有負責垃圾回收機制的線程。這種在在一個進程中有多個線程執行的方式,就是多線程。
多線程的出現能讓程序產生同時運行的效果,提高程序的運行效率。例如:在虛擬機啓動後,進程中執行主線程時,一般會根據程序代碼,在堆內存中產生很多對象,而對象調用完後,就成了垃圾,如果不及時清理,垃圾過多容易造成內存不足,影響程序的運行。所以如果只有主線程運行,程序的效率可能會很低;而如果有一個負責垃圾回收機制的線程運行時,就會對堆內存中的垃圾進行清理,保證了內存的穩定,就保證了程序的運行效率。
二、創建線程
有兩種創建線程的方式:繼承Thread類和實現Runnable接口。
(1)繼承Thread類
Thread類是Java提供的對線程這類事物描述的類,通過繼承Thread類,複寫其run方法來創建線程。步驟如下:
1.定義一個類繼承Thread。
2.覆蓋Thread中的run方法。將自定義的代碼放在run方法中,讓線程運行時,執行這些代碼。
3.創建這個類的對象。相當於創建一個線程。然後用該對象調用線程的start方法。該方法的作用是:啓動線程,調用run方法。注意如果直接用對象調用run方法,相當於沒有啓動創建的線程,還是隻有主線程在執行。
覆蓋run方法的原因:Thread類用於描述線程。該類就定義了一個功能,用於存儲線程要執行的代碼。該存儲功能就是run方法。也就是說,Thread類中的run方法,用於存儲線程要運行的代碼。
下面通過一段程序,演示如何用繼承方法創建線程,如下:
/**
需求:創建兩個線程,和主線程交替運行。
*/
class MyThread extends Thread
{
//覆蓋父類的run方法,存入運行代碼
public void run(){
for(int x=0;x<1000;x++)
System.out.println(Thread.currentThread().getName()+"在運行");
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//創建線程
MyThread mt1=new MyThread();
MyThread mt2=new MyThread();
//啓動線程
mt1.start();
mt2.start();
for(int x=0;x<1000;x++)
System.out.println("Hello World!");
}
}
主線程和創建的線程會交替執行,因爲CPU是隨機地選擇要執行的線程。這種方法可以創建線程,但如果定義的類已經是其他類的子類了,就無法再繼承Thread類了,所以Java提供了另一中創建線程的方式,通過實現Runnable接口的方式。(2)實現Runnable接口
實現Runnable接口,覆蓋run方法。這種創建線程的方式避免了單繼承的侷限性,在定義創建線程時,一般都使用這種方式。具體步驟如下:
1.定義一個類實現Runnable的接口。
2.覆蓋Runnable接口中的run方法。將線程要運行的代碼存放在該run方法中。
3.通過Thread類創建線程對象,並將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造方法。這樣做是因爲自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。
4.調用Thread類中start方法啓動線程。start方法會自動調用Runnable接口子類的run方法。
下面是一個簡單的售票程序,應用的就是實現這種創建線程的方式,如下:
/**
需求:簡單的賣票程序。
多個窗口同時買票。
*/
class Ticket implements Runnable
{
private int ticket = 100;
//複寫run方法
public void run()
{
while(true)
{
if(ticket>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
//創建4個線程,表示4個同時售票的窗口
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//啓動線程,4個窗口開始同時售票。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
這種創建線程的方式,是把線程要運行的代碼放在Runnable接口的子類的run方法中;而繼承Thread類是把要運行的代碼放在Thread的子類的run方法中。在以後的操作中一般用到的都是實現Runnable接口的方式。三、線程的運行狀態
首先看一下表示線程運行狀態的圖例:
被創建:等待啓動,調用start啓動。如果線程已經啓動,處在運行時,再次調用start方法,沒有意義,會提示線程狀態異常。
運行狀態:具有執行資格和執行權。
臨時狀態(阻塞):有執行資格,但沒有執行權。
凍結狀態:遇到sleep(time)方法和wait()方法時,失去執行資格和執行權,sleep方法時間到或者調用notify()方法時,獲得執行資格,變爲臨時狀態。
消亡狀態:stop()方法,或者run方法結束。
四、線程安全
當多條語句在操作多個線程的共享數據時,當一個線程對多條語句只執行了一部分,還沒有執行完時,另一個線程可能就會參與進來執行,這樣會導致共享數據的錯誤。線程的安全問題一旦出現對程序的影響很大。所以Java中提供瞭解決線程安全問題的方法,叫做同步(synchronized),就是對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行,這樣就解決了線程的安全問題。同步中分爲兩種解決方法,一種是同步代碼塊,另一種是同步函數
(1)同步代碼塊
格式:synchronized(對象){需要被同步的代碼}
就以上面售票的程序爲例,利用同步代碼塊,確保線程安全。如下:
class Ticket implements Runnable
{
private int ticket = 100;
//定義用於使用同步的對象。
Object obj = new Object();
public void run()
{
while(true)
{
//給程序實現同步
synchronized(obj)
{
if(ticket>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
}
}
}
}
}
class TicketDemo2
{
public static void main(String[] args)
{
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
在同步代碼塊中,對象就如同一把鎖。持有鎖的線程纔可以在同步中執行。沒有持有鎖的線程即使獲得CPU的執行權,也進不去,因爲沒有獲取鎖。
(2)同步函數
格式:就是在函數上加上synchronized即可。因爲非靜態函數需要被對象調用,所以非靜態函數中都有一個所屬對象引用,即this。同步函數使用的鎖就this
下面還以售票的程序,來用同步函數的格式,實現同步。如下:
class Ticket implements Runnable
{
private int ticket = 100;
public void run()
{
while(true)
show();
}
//通過同步函數,實現同步。
public synchronized void show()
{
if(ticket>0)
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
}
}
class TicketDemo2
{
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();
}
}
無論使用上面兩種方法的哪一種,都必須保證存在兩個或兩個以上的線程,並且這些線程都是在使用同一個鎖,否則實現不了不同,線程還是會有問題。在使用同步時要明確哪些是多線程的運行代碼,哪些是多線程的共享數據以及運行代碼中哪些是用來操作共享數據,明確了這些關鍵點之後,定義的同步會保證線程的安全同步解決了多線程的安全問題,但多個線程需要判斷鎖,較爲消耗資源。
注意,因爲靜態函數中沒有被對象調用,所以它內部沒有對象引用,這時對一個靜態函數同步,它的鎖肯定不是this,而是它所屬的類對應的字節碼文件對象。格式爲類名.class。因爲靜態函數進內存時,內存中一定有它所在的類的字節碼文件對象。所以在靜態函數同步時,就以字節碼文件對象作爲鎖。
在我們之前學到的單例設計模式中,懶漢式需要方法調用時,纔會創建對象。但如果是在多線程中調用此方法時,就容易出現安全問題,保證不了對象在內存中的唯一性,所以這時也要使用到同步。如下:
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{ //使用同步代碼塊,效率稍高
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}
五、線程間通信
就是多個線程在操作同一個資源,但是操作的動作不同。爲了實現一個流程,不同的操作動作需要交替,這時需要用到wait、notify、notifyAll等方法。如下:
class Resource
{
private String name;
private String sex;
//定義判斷標識
boolean flag;
//定義同步函數,表示輸入
public synchronized void input(String name,String sex)
{ //如果已有資源,等待輸入
if(!flag)
try{wait();}catch(Exception e){}
this.name=name;
this.sex=sex;
flag=false;
//喚醒等待線程
notify();
}
//表示輸出
public synchronized void output()
{
//等待輸出
if(flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+name+"........."+sex);
flag=true;
//喚醒等待線程
notify();
}
}
//定義輸入線程
class InputDemo implements Runnable
{
private Resource res;
InputDemo(Resource res)
{
this.res=res;
}
public void run(){
int x=0;
while(true){
if(x==0)
res.input("小明","男");
else
res.input("haha","man");
x=(x+1)%2;
}
}
}
//定義輸出線程
class OutputDemo implements Runnable
{
private Resource res;
OutputDemo(Resource res)
{
this.res=res;
}
public void run(){
while(true)
res.output();
}
}
class Test
{
public static void main(String[]args)
{
Resource res=new Resource();
//創建並啓動線程
new Thread(new InputDemo(res)).start();
new Thread(new OutputDemo(res)).start();
}
}
執行結果爲在控制檯上交替打印"線程名"“小明”...........“男”和"線程名"“hehe”..........“man”。兩個需要知道的知識點: (1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?
1.這些方法存在與同步中。
2.使用這些方法時必須要標識所屬的同步的鎖。同一個鎖上wait的線程,只可以被同一個鎖上的notify喚醒。
3. 鎖可以是任意對象,所以任意對象調用的方法一定定義Object類中。
(2)wait(),sleep()有什麼區別?
wait():釋放cpu執行權,釋放鎖。
sleep():釋放cpu執行權,不釋放鎖。
在實際開發中,有時存在每個不同的操作動作都有多個線程在執行。如下:
/**
需求:多個生產者生產商品,每生產一件商品消費者就購買該商品。
*/
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)
{
//循環判斷被喚醒線程的標識
while(flag)
try{wait();}catch(Exception e){}
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
flag = true;
//喚醒所有等待的線程
notifyAll();
}
public synchronized void out()
{
//循環判斷被喚醒線程的標識
while(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name);
flag = false;
//喚醒所有等待的線程
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();
}
}
}
對於多個生產者和消費者,爲什麼要定義while判斷標示:因爲被喚醒的等待線程可能有多個,讓被喚醒的線程再一次判斷標識。定義notifyAll是因爲需要喚醒對方線程,只定義notify,容易只喚醒本方線程,導致程序中的線程都停掉。
JDK1.5中提供了多線程升級解決方案。將同步synchronized替換成顯示的Lock操作,將Object中的wait、notify、notifyAll,替換成了Condition對象。該對象可以通過Lock鎖獲取,並支持多個相關的Condition對象。在這種方法中,實現了本方只喚醒對方的操作。如下:
/**
需求:多個生產者生產商品,每生產一件商品消費者就購買該商品。
*/
class ReflectTest1
{
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;
//定義Lock對象
Lock lock=new ReentrantLock();
//創建Conditon對象,用來喚醒對方線程。
Condition condition_con=lock.newCondition();
Condition condition_pro=lock.newCondition();
public void set(String name)
{ //實現鎖
lock.lock();
try{
while(flag)
try{condition_pro.await();}catch(Exception e){}
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
flag = true;
//喚醒對方線程
condition_con.signal();
}
//釋放鎖的動作一定要執行
finally{
lock.unlock();
}
}
public synchronized void out()
{
//實現鎖
lock.lock();
try{
while(!flag)
try{condition_con.await();}catch(Exception e){}
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;
}
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();
}
}
}
注意,釋放鎖的動作一定要執行,所以放在finally語句中。
六、停止線程
停止線程只有一種方法,就是讓run方法結束。
開啓線程運行,運行代碼通常是循環結構,只要控制住循環,就可以讓run方法結束,線程就結束了,這時一般需要通過定義標識,通過標識的變化來實現。如下:
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);
<span style="white-space:pre"> </span> int num = 0;
<span style="white-space:pre"> </span>while(true)
{
if(num++ == 60)
{ //調用改變標識的辦法,結束線程。
st.changeFlag();
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
}
}
但是當線程處於凍結狀態時,就不會讀到標識,線程就不會結束。這時需要對凍結進行清除,強制讓線程恢復到運行狀態上來,然後再通過操作標識,讓線程結束,完成清除動作,需要Thread類中的interrupt方法。如下:
class StopThread implements Runnable
{
private boolean flag =true;
public void run()
{
while(flag)
{ //線程凍結
try{Thread.sleep(30)}catch(Exception e){}
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();
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
-------------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流! -------