我們都知道,經過多年的發展和無數Java開發者的不懈努力,Java已經由一門單純的計算機編程語言,逐漸演變成一套強大的以及仍在可持續發展中的技術體系平臺。
雖然,Java設計者們根據不同的技術規范,把Java劃分為3種結構獨立且又彼此依賴的技術體系,分別是Java SE,Java EE 以及Java ME,其中Java EE 在廣泛應用在企業級開發領域中。
除了包括Java API組件外,其衍生和擴充了Web組件,事務組件,分布式組件,EJB組件,消息組件等,并且持續發展到如今,其中,雖然有許多組件現如今不再適用,但是許多組件在我們日常開發工作中,扮演著同樣重要的角色和依舊服務著我們日新月異的業務需求。
綜合Java EE的這些技術,我們可以根據我們的實際需要和滿足我們的業務需求的情況下,可以快速構建出一個具備高性能,結構嚴謹且相對穩定的應用平臺,雖然現在云原生時代異軍突起許多基于非Java的其他技術平臺,但是在分布式時代,Java EE是用于構建SOA架構的首先平臺,甚至基于SpringCloud構建微服務應用平臺也離不開Java EE 的支撐。
個人覺得,Java的持續發展需要感謝Google,正是起初Google將Java作為Android操作系統的應用層編程語言,使得Java可以在PC時代和移動互聯網時代得到快速發展,可以用于手持設備,嵌入式設備,個人PC設備,高性能的集群服務器和大型機器平臺。
當然,Java的發展也不是一帆風順的,也曾被許多開發者詬病和嫌棄,但是就憑Java在行業里能否覆蓋的場景來說,對于它的友好性和包容性,這不由讓我們心懷敬意。其中,除了Java有豐富的內置API供我們使用外,尤其Java對于并發編程的支持,也是我們最難以釋懷的,甚至是我們作為Java開發者最頭疼的問題所在。
雖然,并發編程這個技術領域已經發展了半個世紀了,相關的理論和技術紛繁復雜。那有沒有一種核心技術可以很方便地解決我們的并發問題呢?今天,我們就來一起走進Java領域的并發編程的核心——Java線程機制。
基本概述
在Java中,對于Java語言層面的線程,我們基本都不會太陌生,甚至耳熟能詳。但是在此之前,我們先來探討一下,什么是管程技術?Java 語言在 1.5 之前,提供的唯一的并發原語就是管程,而且 1.5 之后提供的 SDK 并發包,也是以管程技術為基礎的。除此之外,其中C/C++、C# 等高級語言也都支持管程。
關于管程
管程(Monitor)是指定義了一個數據結構和能為并發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據。主要是指提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足后,重新獲得執行權恢復它的互斥訪問。
所謂管程,指的是管理共享變量以及對共享變量的操作過程,讓他們支持并發。翻譯為 Java 領域的語言,就是管理類的成員變量和成員方法,讓這個類是線程安全的。
基本定義
首先,系統中的各種硬件資源和軟件資源均可用數據結構抽象地描述其資源特性,即用少量信息和對該資源所執行的操作來表征該資源,而忽略它們的內部結構和實現細節。
其次,可以利用共享數據結構抽象地表示系統中的共享資源,并且將對該共享數據結構實施的特定操作定義為一組過程。進程對共享資源的申請、釋放和其它操作必須通過這組過程,間接地對共享數據結構實現操作。
然后,對于請求訪問共享資源的諸多并發進程,可以根據資源的情況接受或阻塞,確保每次僅有一個進程進入管程,執行這組過程,使用共享資源,達到對共享資源所有訪問的統一管理,有效地實現進程互斥。
最后,代表共享資源的數據結構以及由對該共享數據結構實施操作的一組過程所組成的資源管理程序共同構成了一個操作系統的資源管理模塊,我們稱之為管程,管程被請求和釋放資源的進程所調用。
綜上所述,管程(Monitor)是指定義了一個數據結構和能為并發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據。主要是指提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足后,重新獲得執行權恢復它的互斥訪問。
基本組成
由上述的定義可知,管程由四部分組成:
管程的名稱;
局部于管程的共享數據結構說明;
對該數據結構進行操作的一組過程;
對局部于管程的共享數據設置初始值的語句
實際上,管程中包含了面向對象的思想,它將表征共享資源的數據結構及其對數據結構操作的一組過程,包括同步機制,都集中并封裝在一個對象內部,隱藏了實現細節。
封裝于管程內部的數據結構僅能被封裝于管程內部的過程所訪問,任何管程外的過程都不能訪問它;反之,封裝于管程內部的過程也僅能訪問管程內的數據結構。
所有進程要訪問臨界資源時,都只能通過管程間接訪問,而管程每次只準許一個進程進入管程,執行管程內的過程,從而實現了進程互斥。
基本特點
管程是一種程序設計語言的結構成分,它和信號量有同等的表達能力,從語言的角度看,管程主要有以下特點:
模塊化,即管程是一個基本程序單位,可以單獨編譯;
抽象數據類型,指管程中不僅有數據,而且有對數據的操作;
信息屏蔽,指管程中的數據結構只能被管程中的過程訪問,這些過程也是在管程內部定義的,供管程外的進程調用,而管程中的數據結構以及過程(函數)的具體實現外部不可見。
基本模型
在管程的發展史上,先后出現過三種不同的管程模型,分別是:Hasen 模型、Hoare 模型和 MESA 模型。其中,現在廣泛應用的是 MESA 模型,并且 Java 管程的實現參考的也是 MESA 模型。
接下來,我們就針對幾種管程模型分別來簡單的說明一下,它們之間的區別。
假設有這樣一個進程同步機制中的問題:如果進程P1因x條件處于阻塞狀態,那么當進程P2執行了x.signal操作喚醒P1后,進程P1和P2此時同時處于管程中了,這是不被允許的,那么如何確定哪個執行哪個等待?
一般來說,我們都會采用如下兩種方式來進行處理:
第一種方式:假如進程 P2進行等待,直至進程P1離開管程或者等待另一個條件
第二種方式:假如進程 P1進行等待,直至進程P2離開管程或者等待另一個條件
綜上所述,三種不同的管程模型采取的方式如下:
1.Hasen 模型
Hansan管程模型,采用了基于兩種的折中處理。主要是規定管程中的所有過程執行的signal操作是過程體的最后一個操作,于是,進程P2執行完signal操作后立即退出管程,因此進程P1馬上被恢復執行。
2.Hoare 模型
Hoare 管程模型,采用第一種方式處理。只要進程 P2進行等待,直至進程P1離開管程或者等待。
3.MESA 模型
MESA 管程模型,采用第二種方式處理。只要進程 P1進行等待,直至進程P2離開管程或者等待。
基本實現
在并發編程領域,有兩大核心問題:互斥和同步。其中:
互斥(Mutual Exclusion),即同一時刻只允許一個線程訪問共享資源
同步(Synchronization),即線程之間如何通信、協作
這兩大問題,管程都是能夠解決的。主要是由于信號量機制是一種進程同步機制,但每個要訪問臨界資源的進程都必須自備同步操作wait(S)和signal(S)。
這樣大量同步操作分散到各個進程中,可能會導致系統管理問題和死鎖,在解決上述問題的過程中,便產生了新的進程同步工具——管程。其中:
信號量(Semaphere):操作系統提供的一種協調共享資源訪問的方法。和用軟件實現的同步比較,軟件同步是平等線程間的的一種同步協商機制,不能保證原子性。而信號量則由操作系統進行管理,地位高于進程,操作系統保證信號量的原子性。
管程(Monitor):解決信號量在臨界區的 PV 操作上的配對的麻煩,把配對的 PV 操作集中在一起,生成的一種并發編程方法。其中使用了條件變量這種同步機制。
綜上所述,這也是Java中,最常見的鎖機制的實現方案,即最典型的實現就是ReenTrantLock為互斥鎖(Mutex Lock) 和synchronized 為同步鎖(Synchronization Lock)。
具體表現
熟悉Java中synchronized 關鍵詞的都應該知道,它是Java語言為開發者提供的同步工具,主要用來解決多線程并發執行過程中數據同步的問題,主要有wait()、notify()、notifyAll() 這三個方法。其中,最關鍵的實現是,當我們在代碼中聲明synchronized 之后,其被聲明部分代碼編譯之后會生成一對monitorenter和monitorexit指令來指定某個同步塊。
在JVM執行指令過程中,一般當遇到monitorenter指令表示獲取互斥鎖時,而當遇到monitorexit指令表示要釋放互斥鎖,這就是synchronized在Java層面實現同步機制的過程。除此之外,如果是獲取鎖失敗,則會將當前線程放入到阻塞讀隊列中,當其他線程釋放鎖時,再通知阻塞讀隊列中的線程去獲取鎖。
由此可見,我們可以知道的是,synchronized 代碼塊是由一對 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。
準確的說,JVM一般通過Monitor來實現monitorenter和monitorexit指令,而且Monitor 對象包括一個阻塞隊列和一個等待隊列。其中,阻塞隊列用來保存鎖競爭失敗的線程,并且它處于阻塞狀態,而等待隊列則用來保存synchronized 代碼塊中調用wait方法后放置的隊列,其調用wait方法后會通知阻塞隊列。
當然,在 Java 6 之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。
這并不意味著,Java是提供信號量這種編程原語來支持解決并發問題的,雖然在《操作系統原理》中,我們知道用信號量能解決所有并發問題,但是在Java中并不是這樣的。
其實,最根本的原因,就是Java 采用的是管程技術,synchronized 關鍵字及 wait()、notify()、notifyAll() 這三個方法都是管程的組成部分。而管程和信號量是等價的,所謂等價指的是用管程能夠實現信號量,也能用信號量實現管程。
特別指出的是,相對于synchronized來說,ReentrantLock主要有以下幾個特點:
從鎖獲取粒度上來看,比synchronized較為細,主要表現在是鎖的持有是以線程為單位而不是基于調用次數。
從線程公平性上來看,ReentrantLock 可以設置公平性(fairness),能減少線程“饑餓”的發生。
從使用角度上來看,ReentrantLock 可以像普通對象一樣使用,所以可以利用其提供的各種便利方法,進行精細的同步操作,甚至是實現 synchronized 難以表達的用例。
從性能角度上來看,synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大。雖然在 Java 6之后 中對其進行了非常多的改進,但在高競爭情況下,ReentrantLock 仍然有一定優勢。
綜上所述,我我相信你對Java中的管程技術已經有了一個明確的認識。接下來,我們便來進入今天的主題——Java線程機制。
更多關于“java培訓”的問題,歡迎咨詢千鋒教育在線名師。千鋒教育多年辦學,課程大綱緊跟企業需求,更科學更嚴謹,每年培養泛IT人才近2萬人。不論你是零基礎還是想提升,都可以找到適合的班型,千鋒教育隨時歡迎你來試聽。