前言
在高并發多線程應用場景中對于synchronized和Lock的使用是很普遍的,這篇文章我們就來進行這些知識點的學習,比如說:公平鎖與非公平鎖、樂觀鎖與悲觀鎖、線程間通信、讀寫鎖、資料髒讀等知識内容。
目錄:
1.同步問題的産生與案例代碼
2.synchronized解決同步問題
3.Lock解決同步代碼問題
4.公平鎖與非公平鎖
5.樂觀鎖與悲觀鎖
6.synchronized與Lock比較
同步問題案例
這個問題在我們日常生活中非常常見,比如說:秒殺物品的庫存資料、火車票剩餘票等就是有同步問題,下面我們通過代碼來解釋這個問題産生的原理:
package com.ckmike.mutilthread; import java.util.concurrent.TimeUnit; /** * SynchronizedQuestionDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-21 下午1:34 * @copyright ckmike **/ public class SynchronizedQuestionDemo { public static void main(String[] args) { // 隻有10張票 TicketService ticketService = new TicketService(10); Thread buy1 = new Thread(ticketService); buy1.setName("buy1"); Thread buy2 = new Thread(ticketService); buy2.setName("buy2"); Thread buy3 = new Thread(ticketService); buy3.setName("buy3"); Thread buy4 = new Thread(ticketService); buy4.setName("buy4"); buy1.start(); buy2.start(); buy3.start(); buy4.start(); } } class TicketService implements Runnable{ private int ticket_store; public TicketService(int ticket_store) { this.ticket_store = ticket_store; } @Override public void run() { while (true) { if (ticket_store > 0) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 輸出賣票資訊 System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--); }else{ break; } } } }

上面的執行結果明顯是不符合我們的預期的,四個線程同時去買票,對餘票資料的判斷存在問題,這就是資料髒讀的場景。
解決辦法:在做售票這個操作時,對于ticket_store的操作同一時刻隻能一個線程操作,那麼我們這裡就會用到鎖這個概念了,對于共享資料java中解決資料髒讀可以通過synchronized和Lock去解決。
現在我們帶着這個問題來了解synchronized和Lock。
synchronized
JVM中每個對象都有一個監控器可以作為鎖。當線程試圖通路同步代碼時,必須先獲得對象鎖(對象螢幕),退出或抛出異常時必須釋放鎖。Synchronzied實作同步的表現形式分為:代碼塊同步和方法同步。
同步代碼塊
在編譯後通過将Monitor Enter指令插入到同步代碼塊的開始處,将Monitor Exit指令插入到方法結束處和異常處,通過反編譯位元組碼可以觀察到。任何一個對象都有一個Monitor(對象監控器)與之關聯,線程執行Monitor Enter指令時,會嘗試擷取對象對應的monitor的所有權,即嘗試獲得對象鎖。
同步方法
從class檔案結構中可知,synchronized方法在method_info結構有ACC_synchronized标記,線程執行時會識别該标記,擷取對應的對象鎖,實作方法同步。
雖然同步方法和同步代碼塊實作細節不同,但本質上都是對一個對象螢幕(monitor)的擷取(對象鎖的擷取)。任意一個對象都擁有自己的螢幕,當同步代碼塊或同步方法時,執行方法的線程必須先獲得該對象的螢幕才能進入同步塊或同步方法,沒有擷取到螢幕的線程将會被阻塞,并進入同步隊列,狀态變為BLOCKED。當成功擷取螢幕的線程釋放了鎖後,會喚醒阻塞在同步隊列的線程,使其重新嘗試對螢幕的擷取。
synchronized解決資料髒讀問題
package com.ckmike.mutilthread; import java.util.concurrent.TimeUnit; /** * SynchronizedQuestionDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-21 下午1:34 * @copyright ckmike **/ public class SynchronizedQuestionDemo { public static void main(String[] args) { // 隻有10張票 TicketService ticketService = new TicketService(10); Thread buy1 = new Thread(ticketService); buy1.setName("buy1"); Thread buy2 = new Thread(ticketService); buy2.setName("buy2"); Thread buy3 = new Thread(ticketService); buy3.setName("buy3"); Thread buy4 = new Thread(ticketService); buy4.setName("buy4"); buy1.start(); buy2.start(); buy3.start(); buy4.start(); } } class TicketService implements Runnable{ private int ticket_store = 100; public TicketService(int ticket_store) { this.ticket_store = ticket_store; } @Override public void run() { while (true) { if (ticket_store > 0) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { if(ticket_store > 0) { // 輸出賣票資訊 System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--); }else { break; } } }else { break; } } } }
上面的方式是使用同步代碼塊實作的同步,解決了資料髒讀的問題。我們也可以通過同步方法來解決這個問題,相比較同步方法,同步代碼塊效率要高些。
上面的代碼我們是對于同一個TicketService執行個體進行多線程操作,是以可以達到同步效果,如果我們使用的是四個不同的執行個體,那麼他們之間就不再是互斥的,因為Java中的鎖是對象鎖,不同執行個體對象鎖是不一樣的,可自行驗證。
如果是靜态同步方法,那麼擷取的應該是該類的鎖,鎖住的是該類,當所有該類的對象(多個對象)在不同線程中調用這個static同步方法時,線程之間會形成互斥,達到同步效果。
結合上面思考:同步執行個體方法,同步類方法,synchronized(this),synchronized(ClassName.class)他們之間的一個關系就出來了,以及他們的應用場景也就出來了。
synchronized線程間通信問題
場景描述:現在我有三個線程分别為線程A,線程B,和線程C,三個線程之間有先後順序的,A操作完了,B才可以操作,B操作完了,C才可以操作。那麼這個時候就需要進行線程之間的通信,然線程知道什麼時候該自己執行。
分析:線上程A執行期間,B線程一直等待A的通知,B執行期間,C一直等待B的通知。
package com.ckmike.mutilthread; /** * BackupDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-20 下午1:51 * @copyright ckmike **/ public class BackupDemo { public static void main(String[] args) { DataTool dataTool = new DataTool(); BackUpA A = new BackUpA(dataTool); BackUpB B = new BackUpB(dataTool); BackUpC C = new BackUpC(dataTool); A.start(); B.start(); C.start(); } } class BackUpA extends Thread{ private DataTool dataTool; public BackUpA(DataTool dataTool) { this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2A(); } } class BackUpB extends Thread{ private DataTool dataTool; public BackUpB(DataTool dataTool) { this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2B(); } } class BackUpC extends Thread{ private DataTool dataTool; public BackUpC(DataTool dataTool){ this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2C(); } } class DataTool{ volatile public String prevA = "A"; // 備份到A資料源 synchronized public void backup2A(){ try { while ("C".equals(prevA)) { wait(); } for(int i=0; i<2;i++){ System.out.println("backup2A資料源"); } prevA = "B"; notifyAll(); }catch (Exception e){ e.printStackTrace(); } } // 備份到B資料源 synchronized public void backup2B(){ try{ while ("A".equals(prevA)){ wait(); } for (int i=0; i<2; i++){ System.out.println("backup2B資料源"); } prevA = "C"; notifyAll(); }catch (Exception e){ e.printStackTrace(); } } // 備份到c資料源 synchronized public void backup2C(){ try{ while ("B".equals(prevA)){ wait(); } for (int i=0; i<2; i++){ System.out.println("backup2C資料源"); } prevA = "C"; notifyAll(); }catch (Exception e){ e.printStackTrace(); } } }
通過volatile和synchronized和wait()\notifyAll()結合實作線程間通信,借助辨別進行線程順序執行。
ReentrantLock
在Java中鎖是用來控制多個線程通路共享資源的方式,一般來說,一個鎖能夠防止多個線程同時通路共享資源(但有的鎖可以允許多個線程并發通路共享資源,比如讀寫鎖,後面我們會分析)。在Lock接口出現之前,Java程式是靠synchronized關鍵字(後面分析)實作鎖功能的,而JAVA SE5.0之後并發包中新增了Lock接口用來實作鎖的功能,它提供了與synchronized關鍵字類似的同步功能,隻是在使用時需要顯式地擷取和釋放鎖,缺點就是缺少像synchronized那樣隐式擷取釋放鎖的便捷性,但是卻擁有了鎖擷取與釋放的可操作性,可中斷的擷取鎖以及逾時擷取鎖等多種synchronized關鍵字所不具備的同步特性。
ReentrantLock解決同步問題
package com.ckmike.mutilthread; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * SynchronizedQuestionDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-21 下午1:34 * @copyright ckmike **/ public class SynchronizedQuestionDemo { public static void main(String[] args) { // 隻有10張票 TicketService ticketService = new TicketService(10); Thread buy1 = new Thread(ticketService); buy1.setName("buy1"); Thread buy2 = new Thread(ticketService); buy2.setName("buy2"); Thread buy3 = new Thread(ticketService); buy3.setName("buy3"); Thread buy4 = new Thread(ticketService); buy4.setName("buy4"); buy1.start(); buy2.start(); buy3.start(); buy4.start(); } } class TicketService implements Runnable{ private int ticket_store = 100; // 預設是非公平鎖 private Lock lock = new ReentrantLock(); public TicketService(int ticket_store) { this.ticket_store = ticket_store; } @Override public void run() { while (true) { if (ticket_store > 0) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); if(ticket_store > 0) { // 輸出賣票資訊 System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--); }else { break; } lock.unlock(); }else { break; } } } }
使用Lock同樣可以解決資料多線程同步問題。
關于ReentrantLock的使用很簡單,隻需要顯示調用,獲得同步鎖,釋放同步鎖即可。
ReentrantLock線程間通信
package com.ckmike.mutilthread; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * BackupDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-20 下午1:51 * @copyright ckmike **/ public class BackupDemo { public static void main(String[] args) { DataTool dataTool = new DataTool(); BackUpA A = new BackUpA(dataTool); BackUpB B = new BackUpB(dataTool); BackUpC C = new BackUpC(dataTool); A.start(); B.start(); C.start(); } } class BackUpA extends Thread{ private DataTool dataTool; public BackUpA(DataTool dataTool) { this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2A(); } } class BackUpB extends Thread{ private DataTool dataTool; public BackUpB(DataTool dataTool) { this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2B(); } } class BackUpC extends Thread{ private DataTool dataTool; public BackUpC(DataTool dataTool){ this.dataTool = dataTool; } @Override public void run() { super.run(); dataTool.backup2C(); } } class DataTool{ volatile public String prevA = "A"; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); // 備份到A資料源 public void backup2A(){ try { lock.lock(); while ("C".equals(prevA)) { condition.await(); } for(int i=0; i<2;i++){ System.out.println("backup2A資料源"); } prevA = "B"; condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } // 備份到B資料源 public void backup2B(){ try{ lock.lock(); while ("A".equals(prevA)){ condition.await(); } for (int i=0; i<2; i++){ System.out.println("backup2B資料源"); } prevA = "C"; condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } // 備份到c資料源 public void backup2C(){ try{ lock.lock(); while ("B".equals(prevA)){ condition.await(); } for (int i=0; i<2; i++){ System.out.println("backup2C資料源"); } prevA = "C"; condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } }
利用Condition可以實作與wait()\notifyAll()一樣的功能。Condition.await()等同于wait(),condition.signalAll()等同與notifyAll()。
公平鎖與非公平鎖
公平鎖:按照線程擷取鎖的順序來配置設定,FIFO。
非公平鎖:是一種擷取鎖的搶占機制,是随機擷取鎖。可能造成某些線程一直拿不到鎖。
synchronized是非公平鎖,ReentrantLock可以通過isFair設定為公平鎖,預設是非公平鎖。
樂觀鎖與悲觀鎖
樂觀鎖與悲觀鎖的概念不是JAVA的概念,而是針對關系型資料庫資料更新時的一種解決方案。
樂觀鎖:就是認為資料沖突的可能性比較小,隻有當事物送出時才回去判斷是否在讀取資料後,是否有其他事務修改了該資料,如果有,則目前事務進行復原。可以這樣簡單了解:一條資料其中有一個字段是version,每次的更新操作都會自動+1,當你讀出這條資料後,如果有其他事務修改了,那麼version就與你送出的version不相等,那麼這個事務就不會被送出。
悲觀鎖:則認為資料沖突是大機率事件,是以每次進行修改之前都會先擷取該資料的鎖,類似于synchronized,是以花費時間較多,效率就會比較低。悲觀鎖是由資料庫自己實作了的,要用的時候,我們直接調用資料庫的相關語句就可以了。而悲觀鎖又分為共享鎖和排他鎖。
共享鎖:就是對于多個不同的事務,對同一個資源共享同一個鎖,類似于一個門多把鑰匙。
排它鎖:排它鎖與共享鎖相對應,類似于一個門隻有一把鑰匙。
行鎖:這個就是字面上的意思,給資料行加上鎖。比如:SELECT * from user where id = 1 lock in share mode; 就是對id=1的資料行加了鎖。這個鎖就是行鎖。
表鎖:給表加上鎖。
這一部分應該是資料庫中的概念,我放到這裡就是因為曾經因為面試問得我一臉懵逼,是以就在這裡簡單的介紹一下,我後面還會寫關于資料庫關于鎖,索引、事務隔離等相關的文章。
ReentrantReadWriteLock
關于ReentrantLock進行更細粒度的鎖,就是這個ReentrantReadWriteLock讀寫鎖,可以針對讀和寫進行加鎖。特别要注意:隻有讀讀是不用加鎖,屬于讀讀共享;但是隻要有寫就一定要加鎖互斥,比如讀寫互斥,寫讀互斥,寫寫互斥。
讀寫案例:
package com.ckmike.mutilthread; import java.util.Date; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * ReadWriteDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-21 下午4:48 * @copyright ckmike **/ public class ReadWriteDemo { public static void main(String[] args) { ReadWriteService readWriteService = new ReadWriteService(); ReadThread read1 = new ReadThread(readWriteService); ReadThread read2 = new ReadThread(readWriteService); read1.setName("A"); read2.setName("B"); read1.start(); read2.start(); WriteThread write1 = new WriteThread(readWriteService); WriteThread write2 = new WriteThread(readWriteService); write1.setName("C"); write2.setName("D"); write1.start(); write2.start(); } } class ReadThread extends Thread{ private ReadWriteService readWriteService; public ReadThread(ReadWriteService readWriteService) { this.readWriteService = readWriteService; } @Override public void run() { super.run(); readWriteService.read(); } } class WriteThread extends Thread{ private ReadWriteService readWriteService; public WriteThread(ReadWriteService readWriteService) { this.readWriteService = readWriteService; } @Override public void run() { super.run(); readWriteService.write(); } } class ReadWriteService{ private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void read(){ try { lock.readLock().lock(); System.out.println("擷取讀鎖:"+Thread.currentThread().getName()+" time:" +new Date().getTime()); TimeUnit.SECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }finally { lock.readLock().unlock(); } } public void write() { try { lock.writeLock().lock(); System.out.println("擷取寫鎖:"+Thread.currentThread().getName()+" time:" +new Date().getTime()); TimeUnit.SECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }finally { lock.writeLock().unlock(); } } }
重入鎖
當一個線程得到一個對象後,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次擷取自己的内部鎖。
Java裡面内置鎖(synchronized)和Lock(ReentrantLock)都是可重入的。
public class SynchronizedTest { public void method1() { synchronized (SynchronizedTest.class) { System.out.println("方法1獲得ReentrantTest的鎖運作了"); method2(); } } public void method2() { synchronized (SynchronizedTest.class) { System.out.println("方法1裡面調用的方法2重入鎖,也正常運作了"); } } public static void main(String[] args) { new SynchronizedTest().method1(); } } public class ReentrantLockTest { private Lock lock = new ReentrantLock(); public void method1() { lock.lock(); try { System.out.println("方法1獲得ReentrantLock鎖運作了"); method2(); } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("方法1裡面調用的方法2重入ReentrantLock鎖,也正常運作了"); } finally { lock.unlock(); } } public static void main(String[] args) { new ReentrantLockTest().method1(); } }
synchronized與ReentrantLock比較
1.差別:
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是内置的語言實作;
2)synchronized在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,是以使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功擷取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
總結:ReentrantLock相比synchronized,增加了一些進階的功能。但也有一定缺陷。
在ReentrantLock類中定義了很多方法,比如:
getHoldCount():查詢目前線程保持此鎖定的個數。
getQueueLength(): 傳回正等待擷取鎖定的線程估計數。
getWaitQueueLength(): 傳回等待與此鎖相關的給定條件Condition的線程估計數。
hasQueuedThread(Thread thread): 查詢指定線程正在等待擷取此鎖定。
hasQueuedThreads(): 擷取是否有線程在等待此鎖定。
hasWaiters():查詢是否有線程在等待與此鎖定有關的Condition條件。
isFair():判斷是否為公平鎖。
isHeldByCurrentThread(): 目前線程是否保持此鎖定。
isLocked(): 查詢此鎖定是否有任意線程保持。
lockInterruptibly(): 目前線程未被中斷,則擷取鎖定。
tryLock():僅在調用時鎖定未被另一個線程保持的情況下擷取該鎖定。
tryLock(long timeout,TimeUnit unit): 如果鎖定在給定等待時間内沒有被另外一個線程保持,且目前線程未被中斷,則擷取該鎖定。
性能:
在性能上來說,如果競争資源不激烈,兩者的性能是差不多的,而當競争資源非常激烈時(即有大量線程同時競争),此時ReentrantLock的性能要遠遠優于synchronized。是以說,在具體使用時要根據适當情況選擇。
在JDK1.5中,synchronized是性能低效的。因為這是一個重量級操作,它對性能最大的影響是阻塞的是實作,挂起線程和恢複線程的操作都需要轉入核心态中完成,這些操作給系統的并發性帶來了很大的壓力。相比之下使用Java提供的ReentrankLock對象,性能更高一些。到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自适應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導緻在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有優化餘地,是以還是提倡在synchronized能實作需求的情況下,優先考慮使用synchronized來進行同步。
特别是你去看看ConcurrentHashMap的鎖實作,在jdk1.7使用的就是segment分段鎖(ReentrantLock實作),但到了jdk1.8就抛棄了segment分段鎖,直接使用synchronized+CAS實作鎖機制。