標籤:電腦程式 thread 後台運行 虛擬機器 計數器
1.多線程基本概念
1.1 進程和線程
進程:一個電腦程式的運行執行個體,包含了需要執行的指令;有自己的獨立地址空間,包含程式內容和資料;不同進程的地址空間是互相隔離的;進程擁有各種資源和狀態資訊,包括開啟的檔案、子進程和訊號處理。
線程:表示程式的執行流程,是CPU調度執行的基本單位;線程有自己的程式計數器、寄存器、堆棧和幀。同一進程中的線程共用相同的地址空間,同時共用進程鎖擁有的記憶體和其他資源。
1.2 Java標準庫提供了進程和線程相關的API
進程主要包括表示進程的java.lang.Process類和建立進程的java.lang.ProcessBuilder類;
線程主要包括表示線程的java.lang.Thread類,在虛擬機器啟動之後,通常只有Java類的main方法這個使用者線程運行(主線程),運行時可以建立和啟動新的線程(子線程);還有一類守護線程(damon thread),守護線程在後台運行,提供者運行時所需的服務。當虛擬機器中啟動並執行所有線程都是守護線程時,虛擬機器終止運行。
2、Thread類和Runnable介面
在java中可有兩種方式實現多線程,一種是繼承Thread類,一種是實現Runnable介面;Thread類是在java.lang包中定義的。一個類只要繼承了Thread類同時覆寫了本類中的run()方法就可以實現多線程操作了。
2.1 建立線程
Java定義了兩種方式用來建立線程:
(1) 通過繼承Thread類,重寫Thread的run()方法,將線程啟動並執行邏輯放在其中
(2)通過實現Runnable介面,執行個體化Thread類
舉例說這兩種方法的使用:在實際應用中,我們經常用到多線程,如車站的售票系統,車站的各個售票口相當於各個線程。當我們做這個系統的時候可能會想到兩種方式來實現,繼承Thread類或實現Runnable介面,現在看一下這兩種方式實現的兩種結果。
(1)通過繼承Thread類
package com.threadtest;class MyThread extends Thread{ private int ticket = 10; private String name; public MyThread(String name){ this.name =name; } public void run(){ for(int i =0;i<500;i++){ if(this.ticket>0){ System.out.println(this.name+"賣票---->"+(this.ticket--)); } } }}public class ThreadDemo { public static void main(String[] args) { MyThread mt1= new MyThread("一號視窗"); MyThread mt2= new MyThread("二號視窗"); MyThread mt3= new MyThread("三號視窗"); mt1.start(); mt2.start(); mt3.start(); }}
運行結果
一號視窗賣票---->10一號視窗賣票---->9二號視窗賣票---->10一號視窗賣票---->8一號視窗賣票---->7一號視窗賣票---->6三號視窗賣票---->10一號視窗賣票---->5一號視窗賣票---->4一號視窗賣票---->3一號視窗賣票---->2一號視窗賣票---->1二號視窗賣票---->9二號視窗賣票---->8三號視窗賣票---->9三號視窗賣票---->8三號視窗賣票---->7三號視窗賣票---->6三號視窗賣票---->5三號視窗賣票---->4三號視窗賣票---->3三號視窗賣票---->2三號視窗賣票---->1二號視窗賣票---->7二號視窗賣票---->6二號視窗賣票---->5二號視窗賣票---->4二號視窗賣票---->3二號視窗賣票---->2二號視窗賣票---->1
(2)通過實現Runnable介面
package com.threadtest;class MyThread1 implements Runnable{ private int ticket =10; private String name; public void run(){ for(int i =0;i<500;i++){ if(this.ticket>0){ System.out.println(Thread.currentThread().getName()+"賣票---->"+(this.ticket--)); } } }}public class RunnableDemo { public static void main(String[] args) { // TODO Auto-generated method stub //設計三個線程 MyThread1 mt = new MyThread1(); Thread t1 = new Thread(mt,"一號視窗"); Thread t2 = new Thread(mt,"二號視窗"); Thread t3 = new Thread(mt,"三號視窗"); t1.start(); t2.start(); t3.start(); }}
運行結果如下一號視窗賣票---->10三號視窗賣票---->9三號視窗賣票---->7三號視窗賣票---->5三號視窗賣票---->4三號視窗賣票---->3三號視窗賣票---->2三號視窗賣票---->1一號視窗賣票---->8二號視窗賣票---->6
為什麼會出現這種結果呢?我們不妨做個比喻:
繼承Thread類的,我們相當於拿出三件事即三個賣票10張的任務分別分給三個視窗,他們各做各的事各賣各的票各完成各的任務,因為MyThread繼承Thread類,所以在new MyThread的時候在建立三個對象的同時建立了三個線程;
實現Runnable的, 相當於是拿出一個賣票10張得任務給三個人去共同完成,new MyThread相當於建立一個任務,然後執行個體化三個Thread,建立三個線程即安排三個視窗去執行。
2.2 Thread類和Runnable介面的區別:
Thread類定義了許多方法,它的衍生類別可以重寫這些方法,在這些方法中,只有run()方法是必須重寫的。當然,在實現Runnable介面時也需要實現這一方法。大多數情況下,如果只想重寫 run() 方法,而不重寫他 Thread 方法,那麼應使用 Runnable 介面。這很重要,因為除非程式員打算修改或增強類的基本行為,否則不應為該類(Thread)建立子類。總的來說,使用Thread類和Runnable類主要有以下區別:
(1)當使用繼承的時候,主要是為了不必重新開發,並且在不必瞭解實現細節的情況下擁有了父類我所需要的特徵。它也有一個很大的缺點,那就是如果我們的類已經從一個類繼承(如小程式必須繼承自 Applet 類),則無法再繼承 Thread 類,
(2)java只能單繼承,因此如果是採用繼承Thread的方法,那麼在以後進行代碼重構的時候可能會遇到問題,因為你無法繼承別的類了,在其他的方面,兩者之間並沒什麼太大的區別。
(3)implement Runnable是面向介面,擴充性等方面比extends Thread好。
(4)使用 Runnable 介面來實現多線程使得我們能夠在一個類中包容所有的代碼,有利於封裝,它的缺點在於,我們只能使用一套代碼,若想建立多個線程並使各個線程執行不同的代碼,則仍必須額外建立類,如果這樣的話,在大多數情況下也許還不如直接用多個類分別繼承 Thread 來得緊湊。
3、 線程同步方法
3.1 synchronized關鍵字
當使用多線程時,有時需要協調兩個以上的活動,這個過程稱通過同步(Synchronization)實現。同步的主要原因有兩個:一是兩個以上的線程需要訪問同一共用資源,而該資源每次只能由一個線程訪問,例如,不允許兩個線程同時對同一檔案進行寫操作;二是當一個線程等待另一個線程引起的事件時,必須要有某種方法來保證前一個線程在事件發生前處於掛起狀態,而在事件發生後,等待的線程恢複執行。
所有的Java對象都有一個與synchronzied關聯的監視器對象(monitor),允許線程在該監視器對象上進行加鎖和解鎖操作。
a、靜態方法:Java類對應的Class類的對象所關聯的監視器對象。
b、執行個體方法:當前對象執行個體所關聯的監視器對象。
c、代碼塊:代碼塊聲明中的對象所關聯的監視器對象。
註:當鎖被釋放,對共用變數的修改會寫入主存;當獲得鎖,CPU緩衝中的內容被置為無效。編譯器在處理synchronized方法或代碼塊,不會把其中包含的代碼移動到synchronized方法或代碼塊之外,從而避免了由於代碼重排而造成的問題。
例:以下方法getNext()和getNextV2() 都獲得了當前執行個體所關聯的監視器對象
public class SynchronizedIdGenerator{ private int value = 0; public synchronized int getNext(){ return value++; } public int getNextV2(){ synchronized(this){ return value++; } } }
3.2 Object類的notify()、wait()和notifyAll()方法
考慮下面一種情形:線程T在一個同步的方法中執行,需要訪問資源R,而資源R又暫時無法訪問,線程T會如何做呢?如果線程T進入了某種等待資源R的輪詢迴圈中,T就鎖定了這個對象,防止其他線程訪問。然而,這並不是一種好的方法,它抵消了多線程環境的編程優勢,更好的解決方案是讓T暫時放棄控制資源R,允許其他線程允許,當資源R可以訪問時,通知線程T,恢複它的執行。這種方法依賴於java提供的線程通訊方法notify()、wait()和notifyAll()。Object類實現了方法notify()、wait()和notifyAll(),因此這些方法是所有對象的一部分,這些方法只能在synchronized內容中調用,他們的使用方法如下:wait: 將當前線程放入,該對象的等待池中,線程A調用了B對象的wait()方法,線程A進入B對象的等待池,並且釋放B的鎖。(這裡,線程A必須持有B的鎖,所以調用的代碼必須在synchronized修飾下,否則直接拋出java.lang.IllegalMonitorStateException異常)。notify:將該對象中等待池中的線程,隨機選取一個放入對象的鎖池,噹噹前線程結束後釋放掉鎖, 鎖池中的線程即可競爭對象的鎖來獲得執行機會。notifyAll:將對象中等待池中的線程,全部放入鎖池。
這是一種生產者和消費者模式,判斷緩衝區是否滿來消費,緩衝區是否空來生產的邏輯。如果用while 和 volatile也可以做,不過本質上會讓線程處於忙等待,佔用CPU時間,對效能造成影響。
java多線程編程