大資料時代隨之而來的就是併發問題。Java開發本身提供了關於鎖的操作。我們知道的有Synchronized。 這個是JVM層面的鎖。操作簡單
Lock的由來
- 因為Synchronized簡單所以不可控制,或者說不是很靈活。Synchronized是已塊進行執行加鎖的。這個時候我們需要通過Lock進行更加靈活的控制。
我們通過tryLock 、 unLock方法進行上鎖釋放鎖。
執行緒之間的互動
- 在多執行緒開發中有的時候我們一個執行緒需要進行等待、休眠操作。這個時候其他執行緒沒必要一直等待。Java中提供了對應的方法進行執行緒切換
- | await/wait | sleep | yield |
---|---|---|---|
釋放鎖 | 釋放 | 不釋放 | 不釋放 |
就緒節點 | notify/notifyall方法後 | 休眠時間後 | 立刻就緒 |
提供者 | Object/Condition | Thread | Thread |
程式碼位置 | 程式碼塊 | 任意 | 任意 |
- 通過上述表格我們可以看出來。線上程中我們可以通過Object.wait方法或者Condition.wait方法進行執行緒掛起的等待(將資源讓給其他執行緒)。在其他執行緒通過Object.notify、Object.notifyall 、 Condition.signal方法進行喚醒當前掛載的執行緒(當前掛載的執行緒不止一個)。
Object.notify | Object.notifyall | Condition.signal |
---|---|---|
隨機喚醒掛載執行緒之一 | 隨機喚醒掛載執行緒之一 | 按順序喚醒當前condition上的掛載執行緒 |
- 這裡主要區別是Object和Condition兩個類。Condition.signal會通知相同Condition上的執行緒就緒(按序通知)
Lock方法簡介
- 通過檢視原始碼我們發現Lock下方法如上。下面我們簡單介紹下方法功能
lock()
- 當前執行緒對資源進行上鎖操作。(如果已被上鎖會一直阻塞住。一直到獲取到鎖)。為什麼避免死鎖的發生,建議在try,catch,finally中結合使用。保證在finally中一定會對資源的釋放
lockInterruptibly()
- 顧名思義就是打斷鎖,在我們對資源進行加鎖被佔用是進行等待時,我們可以通過interrupt()方法打斷在阻塞的執行緒。
trylock()
- trylock就是嘗試去加鎖,如果資源被鎖則返回false,否則返回true表示加鎖成功。
trylock(long,TimeUnit)
- 嘗試加鎖是被佔用,通過TimeUnit指定等待時間段。超時後返回false
unlock()
- unlock就是去釋放鎖佔用的鎖。在finnally中釋放。使用是一定要讓程式碼走到釋放鎖的地方。避免死鎖。
newCondition()
- 和Object的notify不同的是。newCondition會建立一個Condition將與此執行緒進行繫結。這裡可以理解為不同的執行緒繫結在同一個Condition上是一佇列的方式繫結的。當Condition.signal方法是,會從該佇列中取出頭部的執行緒進行喚醒就緒。
使用
- 通過檢視Lock的引用關係得治,JDK中鎖都是繼承Lock實現的。使用最多的應該是ReentrantLock(可重入式鎖) 。 什麼叫可重入式鎖呢就是一個執行緒可以多次呼叫lock方法,對應需要多次呼叫unlock進行解鎖。
Lock保障高併發
package com.github.zxhtom.lock;
import lombok.Data;
import java.util.concurrent.locks.Lock;
/**
* @author 張新華
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:30
* @Copyright © 2020 安元科技有限公司
*/
public class Counter {
private static Counter util = new Counter();
public static Counter getInstance(){
return util;
}
private int index;
public static Counter getUtil() {
return util;
}
public static void setUtil(Counter util) {
Counter.util = util;
}
public int getIndex() {
return index;
}
public void setIndex(Lock lock , int index) {
/*這裡加鎖解鎖是為了顯示可重入性,在外部為加鎖解鎖*/
lock.lock();
this.index = index;
lock.unlock();
}
}
package com.github.zxhtom.lock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
/**
* @author 張新華
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:19
* @Copyright © 2020 安元科技有限公司
*/
public class LockRunnable implements Runnable {
private Lock lock;
public LockRunnable(Lock lock ) {
this.lock = lock;
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
/*lock、unlock之間的業務就能保證同一時刻只有一個執行緒訪問。前提* 是同一個lock物件 , setIndex中也有lock 程式正常執行說明可重* 入
*/
this.lock.lock();
Counter instance = Counter.getInstance();
instance.setIndex(this.lock,instance.getIndex()+1);
this.lock.unlock();
}
}
package com.github.zxhtom.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 張新華
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月01日, 0001 14:24
* @Copyright © 2020 安元科技有限公司
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
int finalI = i;
Thread thread = new Thread(new LockRunnable(lock));
thread.start();
threadList.add(thread);
}
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Counter.getInstance().getIndex());
}
}
- 上述程式碼體現了ReentranLock的可重入性,另外也保障了高併發的問題。如果我們將
LockRunnable
中的加鎖解鎖去掉在執行輸出的結果就會少於1000。在Counter中的加鎖解鎖不去也是會少的。因為那裡的加鎖解鎖只是為了測試可重入性。因為在LockRunnable中的是get、set結合使用的。所以僅僅對set加鎖沒有用的。
Lock期間執行緒掛起
- 上面已經實現了高併發場景下加鎖等待執行了。但是現在我們有一個這樣的場景
場景: 1000個執行緒按名字的奇偶性分組,奇數一組、偶數一組。奇數執行完之後需要將鎖傳遞給同組的執行緒 。
- 根據上述場景我們先考慮一下,第一個執行的執行緒和最後一個執行的執行緒。第一個執行緒毫無疑問是隨機爭取。而最後一個肯定是第一個同組內的最後一個。那麼剩下的一組只能等待前一組全部執行完畢在執行了
- 在開發奇偶分組的場景需求時,我們先回顧下上面的高併發的程式碼。、
- 在介紹lock方法是我著重強調了unlock方法正常需要在try catch finally的finally中執行。但是為什麼我是直接這樣開發。這裡其實是小編開發時大意了。後來想著正好能起一個反面作用。我們上面也看到了不在finally中執行也是可以的。但是在接下來Condition環境下不在finally中unlock就會導致執行緒hold on 了。
LockRunnable改造
- LockRunnalble建構函式裡多接受了Condition類,這個類就是用來分組的.在run方法中我們首先去搶佔鎖,搶到鎖就將執行緒掛起(condition掛起)condition.await()。這樣執行緒就會處於等待狀態。結合Demo類中所有執行緒都會處於awaitting狀態。await阻塞現場後finally裡的也不會被執行。因為執行緒被阻塞整體都不會再運轉了。我們在ReentrantLockDemo類中會通過Condition進行分組喚醒。喚醒的執行緒執行await後面的程式碼。執行完進行同組執行緒喚醒並釋放鎖。這樣就能保證執行緒是分組執行的。
package com.github.zxhtom.lock;
import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author 張新華
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月09日, 0009 14:19
* @Copyright © 2020 安元科技有限公司
*/
public class LockRunnable implements Runnable {
private Lock lock;
private Condition condition;
private int index;
public LockRunnable(Lock lock , Condition condition,int index) {
this.lock = lock;
this.condition = condition;
this.index = index;
}
@Override
public void run() {
try {
this.lock.lock();
//if (index != 0) {
condition.await();
//}
System.out.println(Thread.currentThread().getName());
Counter instance = Counter.getInstance();
instance.setIndex(this.lock,instance.getIndex()+1);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
this.lock.unlock();
}
}
}
ReentrantLockDemo改造
-
在構建執行緒的時候傳入的Condition是按照序號進行傳遞的。我們提前準備了兩個Condition.一個用來存放奇數好執行緒(oddCondition)。一個是儲存偶數號執行緒(evenCondition)。
-
執行緒建立好之後,這個時候由於LockRunnable中condition.await方法早成執行緒阻塞了。後面我們通過不同的Condition進行同組執行緒喚醒。在所有執行緒結束後我們列印執行數也是1000.我在LockRunnable程式碼中輸出了當前執行緒名字。我們通過日誌發現是oddConditon(奇數條件)執行緒先輸出的。50個奇數執行完了才開始evenCondition(偶數條件)。這是因為我們先oddCondition.signal的。這裡讀者可以自行執行程式碼看效果。小編試了試日誌輸出是分組輸出的。
-
在奇偶新增signal的時候間隔時間一定要足夠長。因為在釋放鎖的時候如果這個時候condition前面的lock會搶鎖這樣的話就不會是分組了。因為我們為了測試所以這裡要足夠長
package com.github.zxhtom.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 張新華
* @version V1.0
* @Package com.github.zxhtom.lock
* @date 2020年07月01日, 0001 14:24
* @Copyright © 2020 安元科技有限公司
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
/*奇數*/
Condition oddCondition = lock.newCondition();
/*偶數*/
Condition evenCondition = lock.newCondition();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
int finalI = i;
Condition condition = null;
if (i % 2 == 0) {
condition = evenCondition;
} else {
condition = oddCondition;
}
Thread thread = new Thread(new LockRunnable(lock,condition,i));
thread.start();
threadList.add(thread);
}
try {
lock.lock();
oddCondition.signal();
}finally {
lock.unlock();
}
try {
/*休眠足夠長,目的是不與前面佇列搶鎖.可以調更長時間。
* 這樣測試準確*/
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();
evenCondition.signal();
}finally {
lock.unlock();
}
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Counter.getInstance().getIndex());
}
}
總結
-
我們通過Lock的lock、unlock就可以靈活的控制併發執行順序。上面第二個列子如果我們不在finally中執行unlock就會帶了很多意想不到的效果,讀者可以自己放在一起執行看看效果(在第二個列子中試試).第一個放在一起沒問題是因為業務簡單沒有造成問題的。
-
Condition條件佇列,不同的Condition呼叫await相當於將當前執行緒繫結到該Condition上。當Condition喚醒執行緒內部會將Condition佇列等待的節點轉移到同步佇列上,這裡也是為什麼上面提到兩個Condition間隔時間需要足夠長。因為Condition喚醒佇列上等待的執行緒實際上不是真正的喚醒而是件執行緒新增到通過佇列上,藉由同步佇列的活躍機制喚醒執行緒的,如果間隔時間不長這個時候回去和剛剛Condition新增過來的執行緒進行搶鎖的。Condition喚醒實際上就是重新競爭一把鎖。