一. Lock,ReentrantLock介紹
瞭解lock之前可以對比jdk提供的synchronzied,synchronzied也被用於實現線程同步,但是有些場景下並不靈活,如多個同步方法,每次只能有一個線程訪問;而Lock則可以非常靈活的在代碼中實現同步機制。
Lock 接口的定義
public interface Lock {
// 獲取鎖,若當前lock被其他線程獲取;則此線程阻塞等待lock被釋放
// 如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖
void lock();
// 獲取鎖,若當前鎖不可用(被其他線程獲取);
// 則阻塞線程,等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態
void lockInterruptibly() throws InterruptedException;
// 來嘗試獲取鎖,如果獲取成功,則返回true;
// 如果獲取失敗(即鎖已被其他線程獲取),則返回false
// 也就是說,這個方法無論如何都會立即返回
boolean tryLock();
// 在拿不到鎖時會等待一定的時間
// 等待過程中,可以被中斷
// 超過時間,依然獲取不到,則返回false;否則返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 釋放鎖
void unlock();
// 返回一個綁定該lock的Condtion對象
// 在Condition#await()之前,鎖會被該線程持有
// Condition#await() 會自動釋放鎖,在wait返回之後,會自動獲取鎖
Condition newCondition();
}
ReentrantLock
ReentrantLock是唯一一個實現了Lock的接口的類,叫做可重入鎖,意思是擁有鎖之後,可以再次獲取鎖
二.基本使用
1.模擬上下班場景,公司要求A崗位同時只能安排一個人上班;
package com.lhy.jui.tools.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: lihuiyong
* @DATE : 2019/10/29
* @version:1.0.0
* @description: lock 使用場景
*
* 模擬保安值班,每次只能一個保安值班
*/
public class LockExample {
/**
* 默認創建非公平鎖
*/
private Lock lock = new ReentrantLock();
private void workIng(){
System.out.println(Thread.currentThread().getName() +" 上班!");
}
private void workOff(){
System.out.println(Thread.currentThread().getName() +" 下班!");
}
public void work(){
try{
lock.lock();
workIng();
System.out.println(Thread.currentThread().getName() +"上班中");
Thread.sleep(100);
workOff();
}catch (Exception ex){
System.out.println(Thread.currentThread().getName()+"無法上班,有人還未下班");
}finally {
if(lock != null){
lock.unlock();
}
}
}
public void workTryLock(){
boolean b = lock.tryLock();
System.out.println(b);
if(b){
try {
workIng();
System.out.println(Thread.currentThread().getName() +"上班中");
workOff();
}catch (Exception ex){
ex.printStackTrace();
}finally {
if(lock != null){
lock.unlock();
}
}
}else{
System.out.println(Thread.currentThread().getName()+"無法上班,有人還未下班");
}
}
public static void main(String[] args) throws InterruptedException {
final LockExample lockExample = new LockExample();
List<Thread> threadList = new ArrayList<>(50);
int i = 0 ;
do {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
lockExample.work();
}
},"a_"+i);
Thread b = new Thread(new Runnable() {
@Override
public void run() {
lockExample.work();
}
},"b_"+i);
threadList.add(a);
threadList.add(b);
}while(i++ < 50);
threadList.forEach(s->{
s.start();
});
Thread.sleep(5000);
System.out.println("main over!");
}
}
爲了效果展示,work方法 Thread.sleep(100);
效果如下 :
總結下lock的使用:
a.先創建一個lock對象,Lock lock = new ReentrantLock();
b.work方法進入,需要線程同步,開始先獲取鎖,若鎖被其他線程佔用,則阻塞
c.執行完work方法之後,unlock釋放鎖,釋放鎖和lock是成對出現的,確保unlock要在鎖使用完之後及時釋放,否則造成死鎖;所以一般放在finally裏面釋放鎖.
d.對比上面LockExample中的workTryLock使用的lock.tryLock();tryLock會嘗試獲取鎖,方法有返回值;如果獲取到鎖,返回true;tryLock可以傳等待時間,即在特定的時間內獲取鎖;
而lock.lock跟synchronzied一樣,直接佔用鎖,無需返回值,其他線程進來若鎖被佔用,則會阻塞;
2.Lock和Condtion配合使用
有些場景都線程執行要順序要求,即需要保證併發時入隊和出隊的要求,類似實現線程A的執行需要等待線程B執行完成才執行,
類似CountDownLatch的功能,但是它更多用於阻塞隊列,生產者消費者的模式,實現等待通知機制;
我們模擬下併發線程中的有界隊列
package com.lhy.jui.tools.lock;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: lihuiyong
* @DATE : 2019/10/31
* @version:1.0.0
* @description: lock 和 condition結合實現線程安全中的有界隊列
*/
public class LockWithCondition {
public static class BlockQuene<T> {
/**
* 隊列
*/
private List quene = new LinkedList<T>();
/**
* 隊列大小
*/
private int queneLimit;
/**
* lock
*/
private Lock lock = new ReentrantLock();
/**
* take操作
*/
private Condition takeCondition = lock.newCondition();
/**
* put操作
*/
private Condition putCondition = lock.newCondition();
public BlockQuene(int queneLimit) {
this.queneLimit = queneLimit;
}
/**
* 入隊
*/
public void pushQuene(T t) {
lock.lock();
try {
if (quene.size() == queneLimit) {
//隊列已滿,阻塞put操作
putCondition.await();
}
quene.add(t);
takeCondition.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (lock != null) {
lock.unlock();
}
}
}
/**
* 出隊
*/
public T popQuene() {
lock.lock();
T t = null;
try {
if (quene.isEmpty()) {
//隊列爲空,阻塞take操作
takeCondition.await();
}
putCondition.signal();
t = (T)quene.remove(0);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (lock != null) {
lock.unlock();
}
}
return t;
}
}
public static class ThreadPop implements Runnable{
private BlockQuene<Integer> bolckQuene;
public ThreadPop(BlockQuene<Integer> bolckQuene){
this.bolckQuene = bolckQuene;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" will pop.....");
Integer i = bolckQuene.popQuene();
System.out.println(" i="+i.intValue()+" alread pop");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class ThreadPush implements Runnable{
private BlockQuene<Integer> bolckQuene;
public ThreadPush(BlockQuene<Integer> bolckQuene){
this.bolckQuene = bolckQuene;
}
@Override
public void run() {
int i = 5;
while (i> 0){
try {
Thread.sleep(5000);
System.out.println(" i="+i+" will push");
bolckQuene.pushQuene(i--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BlockQuene b = new BlockQuene(10);
Thread threadPop = new Thread(new ThreadPop(b));
Thread threadPush = new Thread(new ThreadPush(b));
threadPush.start();
threadPop.start();
}
}
運行效果 :
Condition與Lock配套使用,通過 lock.newConditin()
進行實例化
BlockQuene 類主要兩個方法,一個入列一個出列,takeCondition 和 putCondition分別用於出列和入列的阻塞;通過pushQuene和popQuene實現入出隊列的操作;
當popQuene時,如果隊列爲空,則用takeCondition 阻塞線程,若隊列不爲空,則直接pop,同時通知putCondition可以push;
當pushQuene時,如果隊列已滿,則用putCondition阻塞線程,若隊列未滿,加入隊列同時通知takeCondition 可以pop了;
掌握這個原理之後,我們再去了解下阻塞隊列BlockingQueue就比較簡單了,後續會介紹BlockingQueue。