在Java多線程中,可使用synchronized關鍵字來實現線程之間同步互斥,在JDK1.5之後,Java類庫中新增了Lock接口用來實現相似的鎖功能。下面會逐一介紹關於Java類庫中所提供的鎖功能。編程
鎖能夠理解爲對共享數據進行保護的許可證,對於同一把鎖保護的共享數據而言,任何線程對這些共享數據的訪問都須要先持有該鎖。一把鎖只能同時被一個線程持有,當以一個該鎖的持有線程對共享數據訪問結束以後必須釋放該鎖,以便讓其餘線程持有。鎖的持有線程在鎖的得到和鎖的釋放之間的這段時間所執行的代碼被稱爲臨界區。設計模式
鎖可以保護共享數據以實現線程安全,鎖的主要做用有保障原子性、保障可見性和保障有序性。因爲鎖具備互斥性,所以當線程執行臨界區中的代碼時,其餘線程沒法作到干擾,臨界區中的代碼也就具備了不可分割的原子特性。數組
鎖具備排他性,即一個鎖一次只能被一個線程持有,這種鎖又被稱之爲排他鎖或互斥鎖。固然,新版本的JDK中爲了性能優化還推出了另外一種鎖——讀寫鎖,讀寫鎖是做爲了排它鎖的一種改進而存在的。緩存
按照Java虛擬機對鎖的實現方式劃分,Java平臺中的鎖包括內部鎖(主要是經過synchronized實現)和顯式鎖(主要是經過Lock接口及其實現類實現),下文將逐一介紹。安全
公平鎖和非公平鎖:性能優化
鎖Lock分爲"公平鎖"和"非公平鎖",公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進先出順序。非公平鎖就是一種獲取鎖的搶佔機制,是隨機得到鎖的,和公平鎖不同的就是先來的不必定先獲得鎖,這個方式可能形成某些線程一直拿不到鎖,結果也就是不公平的了。數據結構
內部鎖屬於非公平鎖,而顯式鎖不只支持公平鎖並且支持非公平鎖。多線程
Java平臺中的任何一個對象都有惟一一個與之關聯的鎖,這種鎖被稱之爲監視器(或者叫內部鎖)。內部鎖是一種排它鎖,它能保證原子性、可見性和有序性。內部鎖就由synchronized關鍵字實現。併發
synchronized能夠修飾方法或者代碼塊。當synchronized修飾方法的時候,該方法內部的代碼就屬於一個臨界區,該方法就屬於一個同步方法。此時一個線程對該方法內部的變量的更新就保證了原子性和可見性,從而實現了線程安全。當synchronized修飾代碼塊的時候,須要一個鎖句柄(一個對象的引用或者是一個能夠返回對象的表達式),此時synchronized關鍵字引導的代碼塊就是臨界區;同步塊的鎖句柄能夠寫爲this關鍵字,此時表示爲當前對象,鎖句柄對應的監視器就被稱之爲相應同步塊的引導鎖。框架
做爲鎖句柄的變量一般以private final修飾,防止鎖句柄變量的值改變以後,致使執行同一個同步塊的多個線程使用不一樣的鎖,從而避免了競態。
同步實例方法至關於以"this"爲引導鎖的同步塊;同步靜態方法至關於以當前類對象爲引導鎖的同步塊。
線程讀內部鎖的申請和釋放均由Java虛擬機負責代爲實施,內部鎖的使用不會致使鎖泄漏,這是由於Java編譯器在將同步塊代碼編譯成字節碼的時候,對臨界區中可能拋出的而程序代碼中又未捕獲的異常進行了特殊的處理,這使得臨界區的代碼即便拋出異常也不會妨礙內部鎖的釋放。
注意Java虛擬機會爲每個內部鎖分配一個入口集用於存放等待得到相應內部鎖的線程,當內部鎖的持有線程釋放當前鎖的時候,多是入口集中處於BLOCKED狀態的線程得到當前鎖也多是處於RUNNABLE狀態的其餘線程。內部鎖的競爭是激烈的,也是不公平的,可能等待了長時間的線程沒有得到鎖,也多是沒有通過等待的線程直接就得到了鎖。
在Java5.0以前,在協調對共享對象的訪問時可使用的機制只有synchronized和volatile。在Java 5.0中增長了一種新的機制:Lock接口(以及其實現類如ReentrantLock等),Lock接口中定義了一組抽象的加鎖操做。與synchronized不一樣的是,synchronized能夠方便的隱式的獲取鎖,而Lock接口則提供了一種顯式獲取鎖的特性。
顯式鎖是自從JDK1.5以後開始引入的排它鎖。顯式鎖是Lock接口的實例,Lock接口的默認實現類是ReentrantLock。
在詳細介紹關於ReentrantLock類的詳細信息以前,先介紹一下鎖的可重入性的概念。
若是一個線程持有一個鎖的時候還能繼續成功的申請該鎖,那麼咱們就稱該鎖是可重入的,不然咱們就稱該鎖是非可重入的。
複製代碼
ReentrantLock是一個可重入鎖,ReentrantLock類與synchronized相似,均可以實現線程之間的同步互斥。但ReentrantLock類此外還擴展了更多的功能,如嗅探鎖定、多路分支通知等,在使用上也比synrhronized更加的靈活。
上面已經提到ReentrantLock是一個既支持公平支持非公平的顯示鎖,因此在實例化ReentrantLock類的時候咱們能夠明確的看到ReentrantLock的一個構造簽名爲ReentrantLock(boolean fair)
,當咱們傳入true的時候獲得的鎖是一個公平鎖。公平鎖的開銷較非公平鎖的開銷大,所以顯式鎖默認使用的是非公平的調度策略。因爲ReentrantLock能夠具備公平性,所以:
默認狀況下使用內部鎖,而當多數線程持有一個鎖的時間相對較長或者線程申請鎖的平均時間間隔相對長的狀況下咱們能夠考慮使用顯式鎖。
讀寫鎖是一種改進型的排它鎖。讀寫鎖容許多個線程能夠同時讀取(只讀)共享變量。讀寫鎖是分爲讀鎖和寫鎖兩種角色的,讀線程在訪問共享變量的時候必須持有相應讀寫鎖的讀鎖,並且讀鎖是共享的、多個線程能夠共同持有的;寫鎖是排他的,以一個線程在持有寫鎖的時候,其餘線程沒法得到相應鎖的寫鎖或讀鎖。總之,讀寫鎖經過讀寫鎖的分離從而提升了併發性。
ReadWriteLock接口是對讀寫鎖的抽象,其默認的實現類是ReentrantReadWriteLock。ReadWriteLock定義了兩個方法readLock()和writeLock(),分別用於返回相應讀寫鎖實例的讀鎖和寫鎖。這兩個方法的返回值類型都是Lock。
讀寫鎖主要用於讀線程持有鎖的時間比較長的情景下。
多個線程共享同一個非線程安全對象時,咱們每每採用鎖來保證線程安全性。可是,鎖也有其弊端,好比鎖的開銷和在使用鎖的時候容易發生死鎖等。因此在Java中也提供了一些對於某些狀況下替代鎖的同步機制解決方案,如volatile關鍵字、final關鍵字、static關鍵字、原子變量以及各類併發容器和框架,這些大多數內容我將之後介紹;此外咱們還能夠採用必定的多線程設計模式來完成多線程的同步。
首先介紹在併發程序設計中,咱們使用和共享對象能夠採用的一些策略。上面所提到的Java內置的一些工具類和關鍵字以及咱們所採用的設計模式大多都基於這些策略的思想。
這裏我首先介紹其中的volatile關鍵字、ThreadLocal兩者在鎖的某些功能上的替代做用。
經過volatile關鍵字的使用,咱們能夠保證共享變量的可見性和有序性。不一樣於常見的鎖,在原子性方面,volatile僅能保障寫volatile變量操做的原子性,沒有鎖的排他性;此外,volatile關鍵字的使用不會引發上下文的切換,所以volatile常被稱爲輕量級鎖。
在多線程編程基礎一文中,我已經初步介紹了Java的內存模型。volatile最主要的就是實現了共享變量的內存可見性,其實現的原理是:volatile變量的值每次都會從高速緩存或者主內存中讀取,對於volatile變量,每個線程再也不會有一個副本變量,全部線程對volatile變量的操做都是對同一個變量的操做。
volatile變量的開銷包括讀變量和寫變量兩個方面。volatile變量的讀、寫操做都不會致使上下文的切換,所以volatile的開銷比鎖小。可是volatile變量的值不會暫存在寄存器中,所以讀取volatile變量的成本要比讀取普通變量的成本更高。
ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程能夠根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
ThreadLocal採用的是上述策略中的第一種設計思想——採用線程的特有對象.採用線程的特有對象,咱們能夠保障每個線程都具備各自的實例,同一個對象不會被多個線程共享,ThreadLocal是維護線程封閉性的一種更加規範的方法,這個類能使線程中的某個值與保存值的對象關聯起來,從而保證了線程特有對象的固有線程安全性。
ThreadLocal類至關於線程訪問其線程特有對象的代理,即各個線程經過這個對象能夠建立並訪問各自的線程特有對象,泛型T指定了相應線程持有對象的類型。一個線程可使用不一樣的ThreadLocal實例來建立並訪問其不一樣的線程持有對象。多個線程使用同一個ThreadLocal實例所訪問到的對象時類型T的不一樣實例。代理的關係圖以下:
ThreadLocal提供了get和set等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,所以get老是能返回由當前執行線程在調用set時設置的最新值。其主要使用的方法以下:
public T get(): 獲取與當前線程中ThreadLocal實例關聯的線程特有對象。
public void set(T value):從新關聯當前線程中ThreadLocal實例所對應的線程特有對象。
protected T initValue():若是沒有調用set(),在初始化threadlocal對象的時候,該方法的返回值就是當前線程中與ThreadLocal實例關聯的線程特有對象。
public void remove():刪除當前線程中ThreadLocal和線程特有對象的關係。
複製代碼
那麼ThreadLocal底層是如何實現Thread持有本身的線程特有對象的?查看set()方法的源代碼:
能夠看到,當咱們調用threadlocal的set方法來保存當前線程的特有對象時,threadlocal會取出當前線程關聯的threadlocalmap對象,而後調用ThreadLocalMap對象的set方法來進行當前給定值的保存。每個Thread都會維護一個ThreadLocalMap對象,ThreadLocalMap是一個相似Map的數據結構,可是它沒有實現任何Map的相關接口。ThreadLocalMap是一個Entry數組,每個Entry對象都是一個"key-value"結構,並且Entry對象的key永遠都是ThreadLocal對象。當咱們調用ThreadLocal的set方法時,實際上就是以當前ThreadLocal對象自己做爲key,放入到了ThreadLocalMap中。
可能發生內存泄漏:
經過查看Entry結構可知,Entry屬於WeakReference類型,所以Entry不會阻止被引用的ThreadLocal實例被垃圾回收。當一個ThreadLocal實例沒有對其可達的強引用時,這個實例就能夠被垃圾回收,即其所在的Entry的key會被置爲null,可是若是建立ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,從而發生內存泄露。
解決內存泄漏的最有效方法就是,在使用完ThreadLocal以後,要注意調用threadlocal的remove()方法釋放內存。