JAVA多線程

#1、進程和線程html

進程:每一個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。java

線程:同一類線程共享代碼和數據空間,每一個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。編程

線程和進程同樣分爲五個階段:建立、就緒、運行、阻塞、終止。多線程

多進程是指操做系統能同時運行多個任務(程序)。併發

多線程是指在同一程序中有多個順序流在執行。app

主線程:JVM調用程序main()所產生的線程。ide

當前線程:這個是容易混淆的概念。通常指經過Thread.currentThread()來獲取的進程。函數

後臺線程:指爲其餘線程提供服務的線程,也稱爲守護線程。JVM的垃圾回收線程就是一個後臺線程。用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束this

前臺線程:是指接受後臺線程服務的線程,其實前臺後臺線程是聯繫在一塊兒,就像傀儡和幕後操縱者同樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程建立的線程默認也是前臺線程。能夠經過isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。操作系統

線程狀態轉換

  • 新建狀態(New):新建立了一個線程對象。

  • 就緒狀態(Runnable):線程對象建立後,其餘線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

  • 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

  • 阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權,暫時中止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的狀況分三種:

    1. 等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。

    2. 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。

    3. 其餘阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。

  • 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

輸入圖片說明

線程的調度

  • 調整線程優先級 Java線程有優先級,優先級高的線程會得到較多的運行機會
    Java線程的優先級用整數表示,取值範圍是1~10,Thread類有如下三個靜態常量:
// 線程能夠具備的最高優先級,取值爲10。
static int MAX_PRIORITY
// 線程能夠具備的最低優先級,取值爲1。        
static int MIN_PRIORITY
 //  分配給線程的默認優先級,取值爲5。
static int NORM_PRIORITY

Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。

每一個線程都有默認的優先級。主線程的默認優先級爲Thread.NORM_PRIORITY。線程的優先級有繼承關係,好比A線程中建立了B線程,那麼B將和A具備相同的優先級。JVM提供了10個線程優先級,但與常見的操做系統都不能很好的映射。若是但願程序能移植到各個操做系統中,應該僅僅使用Thread類有如下三個靜態常量做爲優先級,這樣能保證一樣的優先級採用了一樣的調度方式。

  • 線程睡眠
    Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒爲單位。當睡眠結束後,就轉爲就緒(Runnable)狀態。sleep()平臺移植性好。

  • 線程等待
    Object類中的wait()方法,致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 同樣。

  • 線程讓步
    Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

  • 線程加入
    join()方法,等待其餘線程終止。在當前線程中調用另外一個線程的join()方法,則當前線程轉入阻塞狀態,直到另外一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。

  • 線程喚醒
    Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。若是全部線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現作出決定時發生。線程經過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其餘全部線程進行競爭;例如,喚醒的線程在做爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。相似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的全部線程。 注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,再也不介紹。由於有死鎖傾向。

wait()和sleep()

  1. 共同點
  • 他們都是在多線程的環境下,均可以在程序的調用處阻塞指定的毫秒數,並返回;
  • wait()和sleep()均可以經過interrupt()方法 打斷線程的暫停狀態 ,從而使線程馬上拋出InterruptedException。
  1. 區別
  • Thread類的方法:sleep(),yield()等,Object的方法:wait()和notify()等
  • 每一個對象都有一個鎖來控制同步訪問。Synchronized關鍵字能夠和對象的鎖交互,來實現線程的同步。 sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其餘線程可使用同步控制塊或者方法。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep能夠在任何地方使用
  • sleep必須捕獲異常,而wait,notify和notifyAll不須要捕獲異常
  • sleep()睡眠時,保持對象鎖,仍然佔有該鎖;而wait()睡眠時,釋放對象鎖。

線程類的一些經常使用方法

 sleep(): 強迫一個線程睡眠N毫秒。 
  isAlive(): 判斷一個線程是否存活。 
  join(): 等待線程終止。 
  activeCount(): 程序中活躍的線程數。 
  enumerate(): 枚舉程序中的線程。 
    currentThread(): 獲得當前線程。 
  isDaemon(): 一個線程是否爲守護線程。 
  setDaemon(): 設置一個線程爲守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束) 
  setName(): 爲線程設置一個名稱。 
  wait(): 強迫一個線程等待。 
  notify(): 通知一個線程繼續運行。 
  setPriority(): 設置一個線程的優先級。

#2、多線程 在java中要想實現多線程,有兩種手段,一種是繼續Thread類,另一種是實現Runable接口。

區別
若是一個類繼承Thread,則不適合資源共享。可是若是實現了Runable接口的話,則很容易的實現資源共享。

實現Runnable接口比繼承Thread類所具備的優點:

1):適合多個相同的程序代碼的線程去處理同一個資源

2):能夠避免java中的單繼承的限制

3):增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立

main方法其實也是一個線程。在java中因此的線程都是同時啓動的,至於何時,哪一個先執行,徹底看誰先獲得CPU的資源。

在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。由於每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每個JVM實際上就是在操做系統中啓動了一個進程。

#3、線程同步 java容許多線程併發控制,當多個線程同時操做一個可共享的資源變量時(如數據的增刪改查), 將會致使數據不許確,相互之間產生衝突,所以加入同步鎖以免在該線程沒有完成操做以前,被其餘線程的調用, 從而保證了該變量的惟一性和準確性。

  1. Synchronized(基於JVM)
  • synchronized aMethod(){}
  • synchronized static aStaticMethod{}
  • synchronized(this){/區塊/}
  • synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類須要你顯式的指定它的某個方法爲synchronized方法;

synchronized關鍵字能夠做爲函數的修飾符,也可做爲函數內的語句,也就是平時說的同步方法和同步語句塊。若是再細的分類,synchronized可做用於instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。

  1. ReentrantLock (重入鎖) (基於方法)
  • lock() :若是獲取了鎖當即返回,若是別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖
  • tryLock() :若是獲取了鎖當即返回true,若是別的線程正持有鎖,當即返回false
  • tryLock(long timeout,TimeUnit unit): 若是獲取了鎖定當即返回true,若是別的線程正持有鎖,會等待參數給定的時間,在等待的過程當中,若是獲取了鎖定,就返回true,若是等待超時,返回false
  • lockInterruptibly:若是獲取了鎖定當即返回,若是沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷
class Bank {
            
            private int account = 100;
            //須要聲明這個鎖
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //這裏再也不須要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
                
            }
        }

ReentrantLock 擁有Synchronized相同的併發性和內存語義,此外還多了 鎖投票,定時鎖等候和中斷鎖等候;示例:線程A和B都要獲取對象O的鎖定,假設A獲取了對象O鎖,B將等待A釋放對O的鎖定;若是使用 synchronized ,若是A不釋放,B將一直等下去,不能被中斷 ;若是 使用ReentrantLock,若是A不釋放,可使B在等待了足夠長的時間之後,中斷等待,而幹別的事情;

使用ReentrantLock要注意及時釋放鎖,不然會出現死鎖,一般在finally代碼釋放鎖;

  1. Atomic(原子變量)
  • AtomicInteger
AtomicInteger(int initialValue):建立具備給定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式將給定值與當前值相加
get() :獲取當前值
  • AtomicLong
  • AtomicDouble
class Bank {
        private AtomicInteger account = new AtomicInteger(100);

        public AtomicInteger getAccount() {
            return account;
        }

        public void save(int money) {
            account.addAndGet(money);
        }
    }
  1. 使用特殊域變量(volatile)實現線程同步
    • volatile關鍵字爲域變量的訪問提供了一種免鎖機制,
    • 使用volatile修飾域至關於告訴虛擬機該域可能會被其餘線程更新,
    • 所以每次使用該域就要從新計算,而不是使用寄存器中的值
    • volatile不會提供任何原子操做,它也不能用來修飾final類型的變量
class Bank {
            //須要同步的變量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //這裏再也不須要synchronized 
            public void save(int money) {
                account += money;
            }
        }

多線程中的非同步問題主要出如今對域的讀寫上,若是讓域自身避免這個問題,則就不須要修改操做該域的方法。 用final域,有鎖保護的域和volatile域能夠避免非同步的問題。

  1. 使用局部變量ThreadLocal實現線程同步
    若是使用ThreadLocal管理變量,則每個使用該變量的線程都得到該變量的副本,副本之間相互獨立,這樣每個線程均可以隨意修改本身的變量副本,而不會對其餘線程產生影響。

    • ThreadLocal() :建立一個線程本地變量
    • get() :返回此線程局部變量的當前線程副本中的值
    • initialValue() : 返回此線程局部變量的當前線程的"初始值"
    • set(T value):將此線程局部變量的當前線程副本中的值設置爲value
public class Bank{
            //使用ThreadLocal類管理共享變量account
            private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
                @Override
                protected Integer initialValue(){
                    return 100;
                }
            };
            public void save(int money){
                account.set(account.get()+money);
            }
            public int getAccount(){
                return account.get();
            }
        }

ThreadLocal與同步機制都是爲了解決多線程中相同變量的訪問衝突問題。前者採用以"空間換時間"的方法,後者採用以"時間換空間"的方式。

  1. 使用阻塞隊列實現線程同步(LinkedBlockingQueue)
    LinkedBlockingQueue 類經常使用方法
    • LinkedBlockingQueue():建立一個容量爲Integer.MAX_VALUE的LinkedBlockingQueue
    • put(E e) : 在隊尾添加一個元素,若是隊列滿則阻塞
    • size(): 返回隊列中的元素個數
    • take(): 移除並返回隊頭元素,若是隊列空則阻塞

#4、生產者和消費者問題 生產者和消費者問題是線程模型中的經典問題:生產者和消費者在同一時間段內共用同一個存儲空間,以下圖所示,生產者向空間裏存放數據,而消費者取用數據,若是不加以協調可能會出現如下狀況:
輸入圖片說明
存儲空間已滿,而生產者佔用着它,消費者等着生產者讓出空間從而去除產品,生產者等着消費者消費產品,從而向空間中添加產品。互相等待,從而發生死鎖。

解決生產者/消費者問題的方法可分爲兩類:
(1)採用某種機制保護生產者和消費者之間的同步;
(2)在生產者和消費者之間創建一個管道。
第一種方式有較高的效率,而且易於實現,代碼的可控制性較好,屬於經常使用的模式。第二種管道緩衝區不易控制,被傳輸數據對象不易於封裝等,實用性不強。

同步問題核心在於:如何保證同一資源被多個線程併發訪問時的完整性。經常使用的同步方法是採用信號或加鎖機制,保證資源在任意時刻至多被一個線程訪問。Java語言在多線程編程上實現了徹底對象化,提供了對同步機制的良好支持。在Java中一共有四種方法支持同步,其中前三個是同步方法,一個是管道方法。 (1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞隊列方法
(4)PipedInputStream / PipedOutputStream

參考文獻:
http://www.cnblogs.com/happyPawpaw/archive/2013/01/18/2865957.html
http://www.cnblogs.com/doit8791/p/4093808.html

#5、JAVA死鎖 Java線程死鎖是一個經典的多線程問題,由於不一樣的線程都在等待那些根本不可能被釋放的鎖,從而致使全部的工做都沒法完成。

主要緣由:多線程下不恰當的使用同步線程方法,好比synchronized;

解決死鎖的經驗法則:當幾個線程都要訪問共享資源A、B、C時,保證使每一個線程都按照一樣的順序去訪問它們,好比都先訪問A,在訪問B和C。

JAVA死鎖參考文獻:
http://blog.csdn.net/hijiankang/article/details/9157365

相關文章
相關標籤/搜索