多執行緒程式設計之synchronized和Lock
前言
在高併發多執行緒應用場景中對於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實現鎖機制。