併發編程有3個源頭性問題:緩存致使的可見性問題,編譯優化致使的有序性問題,以及線程切換致使的原子性問題。解決可見性問題和有序性問題的方法是按需禁用緩存和編譯優化,Java的內存模型就是一種按需禁用緩存和編譯優化的規則,它規定了 JVM 如何提供相關的方法,這些已經在Java內存模型與Hppens-Before規則進行了描述。java
咱們把一個或者多個操做在 CPU 執行過程當中不被中斷的特性稱爲原子性。因爲操做系統的時間片輪起色制,以及高級語言可能包含多個指令,致使一句高級語言在執行過程當中可能出現線程切換。在併發編程中就會由於線程切換致使原子性問題。編程
鎖模型是解決原子性問題的通用方案。線程在進入臨界區以前必須持有鎖,退出臨界區時釋放鎖,此時其餘線程就能再次獲取鎖。segmentfault
鎖與資源之間是 1:N 的關係,即一把鎖能夠保護多個資源。同時要注意不能用本身的鎖保護別人的資源;要讓代碼實現互斥,必須使用同一把鎖。緩存
synchronized
關鍵字是 Java 語言對鎖模型的實現,它能夠修飾方法或者代碼塊,被修飾的方法和代碼塊會隱式地添加lock()
和unlock()
方法。併發
現代操做系統都是基於線程的分時調度系統,CPU會爲線程分配時間片,線程分配都時間片就獲取到CPU的使用權。好比說線程 A 讀取文件,它能夠將本身標記爲「休眠狀態」,讓出 CPU 的使用權。文件讀取完成以後,操做系統再將其喚醒,線程 A 就有機會從新得到 CPU 的使用權。app
線程切換爲何致使併發問題呢?Java 是一門高級語言,高級語言的一條語句每每包含多個 CPU 指定,好比說 count += 1
這條語句,至少包含 3 條 CPU 指令:優化
操做系統以指令爲單位執行,期間伴隨着線程切換。這就致使 count += 1
執行到一半,就有可能碰到線程切換,致使併發問題的產生,以下圖所示:this
咱們把一個或者多個操做在 CPU 執行過程當中不被中斷的特性稱爲原子性,即咱們指望 count += 1
在執行過程當中是原子同樣的,不可分割的總體,線程切換不會在執行這條語句相關的CPU指令時發生,但容許線程切換在count += 1
執行以前或者以後發生。spa
鎖模型是一種解決原子性問題的通用技術方案。在鎖模型中,臨界區是一段要互斥執行的代碼,在進入臨界區以前咱們要執行 lock()
操做持有鎖,只有獲取到鎖的線程才能執行臨界區的代碼;執行完臨界區代碼執行 unlock()
操做釋放鎖,此時其餘線程就能夠嘗試獲取鎖。操作系統
在現實生活中,咱們用鎖來保護咱們的東西,但不能用本身的鎖來鎖別人的東西。在鎖模型中,鎖與臨界區中被保護的資源也有着關聯關係,圖中用箭頭來表示它們之間的關聯。
咱們不能用一把鎖來保護範圍以外的資源,代碼要實現互斥則要使用同一把鎖。
鎖是一種通用的技術方案,Java 語言提供的 synchronized
關鍵字,就是鎖的一種實現。synchronized
關鍵字能夠用來修飾方法,也能夠用來修飾代碼塊,它的使用示例基本上都是下面這個樣子:
class X { // 修飾非靜態方法 synchronized void foo() { // 臨界區 } // 修飾靜態方法 synchronized static void bar() { // 臨界區 } // 修飾代碼塊 Object obj = new Object(); void baz() { synchronized(obj) { // 臨界區 } } }
前面說過,鎖模型中有鎖以及它保護的資源,synchronized 修飾代碼塊的時候鎖顯然是 obj 對象,那麼 synchronized 修飾非靜態方法和靜態方法的時候,它建立的鎖是什麼呢?
Java 中有一條隱式規則:
當修飾靜態方法的時候,鎖定的是當前類的 Class 對象; 當修飾非靜態方法的時候,鎖定的是當前實例對象 this。
至關於
class X { // 修飾靜態方法 synchronized(X.class) static void bar() { // 臨界區 } } class X { // 修飾非靜態方法 synchronized(this) void foo() { // 臨界區 } }
鎖能夠保護一個或者多個資源。咱們能夠用一個範圍較大的鎖,好比說 X.class
保護多個相關的資源;也能夠用不一樣的鎖對被保護資源進行精細化管理,這就叫細粒度鎖。
class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }
這是一段想解決 count += 1
問題的代碼,咱們對 addOne()
使用 synchronized 加上互斥鎖,能夠保證其原子性。根據 Happens-before 管程中鎖的規則:對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖,也能夠保證其可見性。即便是 1000 個線程同時執行 addOne()
也能夠保證 value 增長 1000。
但咱們沒法保證 get()
的可見性,管程中鎖的規則,是隻保證後續對這個鎖的加鎖的可見性,而 get() 方法並無加鎖操做,因此可見性無法保證。因此咱們給 get()
也加上鎖:
class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; } }
此時 get()
和 addOne()
都持有 this 這把鎖,此時 get()
和 addOne()
是互斥的,而且保證了可見性,縮模型以下圖所示:
若是將 value 改成 static 的,addOne()
變爲靜態方法:
class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; } }
此時 get()
和 addOne()
分別持有不一樣的鎖,get()
和 addOne()
不互斥,也就不能保證可見性,就會致使併發問題。
如今要寫一個銀行轉帳的方法,用戶 A 給用戶 B 轉帳,將其轉換成代碼:
class Account { private int balance; // 轉帳 synchronized void transfer( Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
用戶 A 給用戶 B 轉帳 100,要保證 A 的餘額減小 100,B 的餘額增長 100。因爲轉帳操做能夠是併發的,因此要保證轉帳操做沒有併發問題。好比說 A 的餘額只有 100,兩個線程分別執行 A 給 B 轉帳 100,A 給 C 轉帳 100,這兩個線程有可能同時從內存中讀取到 A 的餘額是 100,這就產生了併發問題。
解決這個問題的第一反應,就是給 transfer(Account target, int amt)
加上 synchronized。這樣作真的對麼?transfer()
此時有兩個須要被保護的資源 target.balance
和 this.balance
即別人錢和本身的錢,但咱們使用的鎖是 this
鎖,以下圖所示:
本身的鎖 this
能保護本身的 this.balance
可是沒法保護別人的 target.balance
,就像個人鎖不能即保護我家的東西,又保護你家的東西同樣。
因此咱們須要一把鎖的範圍更大一點,讓它可以覆蓋到全部的被保護資源,好比說傳入同一個對象做爲鎖:
class Account { private Object lock; private int balance; private Account(); // 建立Account時傳入同一個lock對象 public Account(Object lock) { this.lock = lock; } // 轉帳 void transfer(Account target, int amt){ // 此處檢查全部對象共享的鎖 synchronized(lock) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
或者使用類鎖 Accout.class
,因爲 Accoutn.class
是在 Java 虛擬機加載 Account 類時建立的,因此 Account.class
是全部 Account 對象共享且惟一的一把鎖。
class Account { private int balance; // 轉帳 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
Accout.class
就能夠同時保護兩個不一樣對象的臨界區資源: