目錄
- Lock的由來
- 線程之間的互動
- Lock方法簡介
- lock()
- lockInterruptibly()
- trylock()
- trylock(long,TimeUnit)
- unlock()
- newCondition()
- 使用
- Lock保障高并發
- Lock期間線程挂起
- LockRunnable改造
- ReentrantLockDemo改造
- 總結
- # 加入戰隊
- 微信公衆号
大資料時代随之而來的就是并發問題。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的可重入性,另外也保障了高并發的問題。如果我們将
中的加鎖解鎖去掉在運作輸出的結果就會少于1000。在Counter中的加鎖解鎖不去也是會少的。因為那裡的加鎖解鎖隻是為了測試可重入性。因為在LockRunnable中的是get、set結合使用的。是以僅僅對set加鎖沒有用的。LockRunnable
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喚醒實際上就是重新競争一把鎖。
加入戰隊