1 ReentrantLock可重入
同一个线程,如果首次获取到该锁资源,则它就有权力再次获取到该锁,这就是锁的重入。
package com.zs.thread;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptibly {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
fun_1();
}
public static void fun_1() {
lock.lock();
try {
System.out.println("fun_1 lock");
fun_2();
} finally {
lock.unlock();
}
}
private static void fun_2() {
lock.lock();
try {
System.out.println("fun_2 lock");
fun_3();
} finally {
lock.unlock();
}
}
private static void fun_3() {
lock.lock();
try {
System.out.println("fun_3 lock");
} finally {
lock.unlock();
}
}
}
2 ReentrantLock可中断
所谓的中断锁指的是锁在执行时可被中断,也就是在执行时可以接收 interrupt 的通知,从而中断锁执行。 默认情况下 Lock 也是不可中断锁,但是可以通过特殊的“手段”,可以让其变为可中断锁。
不可中断锁的问题是:当出现“异常”时,只能一直阻塞等待,别无其他办法,比如下面这个程序:
下面的这个程序中有两个线程,其中线程 1 先获取到锁资源执行相应代码,而线程 2 在 0.5s 之后开始尝试获取锁资源,但线程 1 执行时忘记释放锁了,这就造成线程 2 一直阻塞等待的情况,实现代码如下:
package com.zs.thread;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 创建线程 1
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("线程 1:获取到锁.");
// 线程 1 未释放锁
});
t1.start();
// 创建线程 2
Thread t2 = new Thread(() -> {
// 先休眠 0.5s,让线程 1 先执行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取锁
System.out.println("线程 2:等待获取锁.");
lock.lock();
try {
System.out.println("线程 2:获取锁成功.");
} finally {
lock.unlock();
}
});
t2.start();
}
}
线程 2 还在阻塞等待获取线程 1 释放锁资源,此时的线程 2 除了等之外,并无其他方法。
然而,中断锁的出现,就可以打破这一僵局,它可以在等待一定时间之后,主动的中断线程 2,以解决线程阻塞等待的问题。 中断锁的核心实现代码是 lock.lockInterruptibly() 方法,它和 lock.lock() 方法作用类似,只不过使用 lockInterruptibly 方法可以优先接收中断的请求,中断锁的具体实现如下:
package com.zs.thread;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
// 创建线程 1
Thread t1 = new Thread(() -> {
// 加锁操作
lock.lock();
System.out.println("线程 1:获取到锁.");
// 线程 1 未释放锁
});
t1.start();
// 创建线程 2
Thread t2 = new Thread(() -> {
// 先休眠 0.5s,让线程 1 先执行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取锁
try {
System.out.println("线程 2:尝试获取锁.");
lock.lockInterruptibly(); // 可中断锁
System.out.println("线程 2:获取锁成功.");
} catch (InterruptedException e) {
System.out.println("线程 2:执行已被中断.");
}
});
t2.start();
// 等待 2s 后,终止线程 2
Thread.sleep(2000);
if (t2.isAlive()) { // 线程 2 还在执行
System.out.println("执行线程的中断.");
t2.interrupt();
} else {
System.out.println("线程 2:执行完成.");
}
}
}
从上述结果可以看出,当我们使用了 lockInterruptibly 方法就可以在一段时间之后,判断它是否还在阻塞等待,如果结果为真,就可以直接将他中断,如上图效果所示。 但当我们尝试将 lockInterruptibly 方法换成 lock 方法之后(其他代码都不变),执行的结果就完全不一样了。
通过显示锁 Lock 的 lockInterruptibly 方法来完成,它和 lock 方法作用类似,但 lockInterruptibly 可以优先接收到中断的通知,而 lock 方法只能“死等”锁资源的释放。
3 ReentrantLock超时时间
如果没有指定时间,则立即失败(没有获取到锁):
package com.zs.thread;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1尝试获取锁");
if(!lock.tryLock()){
System.out.println("没有获取到锁");
return;
}
try {
System.out.println("t1 获取到锁");
}finally {
lock.unlock();
}
}, "t1");
//主线程上锁
lock.lock();
System.out.println("main 获取到锁");
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
在主线程还没释放到锁,t1线程是无法获取到锁的,因此reentrantLock.tryLock()返回false,执行没有锁的相关业务。如果有指定时间,在指定最大时间内没有获取到锁则失败:
package com.zs.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1尝试获取锁");
try {
if(!lock.tryLock(4, TimeUnit.SECONDS)){
System.out.println("没有获取到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("t1 获取到锁");
}finally {
lock.unlock();
}
}, "t1");
//主线程上锁
lock.lock();
System.out.println("main 获取到锁");
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
当等待获取锁的时间大于主线程释放锁的时间,则肯定可以获取到锁。
4 ReentrantLock类与Condition类配合实现等待/通知机制
关键字synchronized与wait()合notify()或notifyAll()结合可以实现等待/通知机制,而ReentrantLock和Condition配合也可以实现等待/通知机制。使用notify()和notifyAll()无法选择性的通知线程,使用Condition的优点就是可以在Lock对象里面创建多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而实现有选择性地进行线程通知,使线程调度更加的灵活。
Condition类方法:
await():使线程等待相当于Object对象中的wait()方法。
await(long time, TimeUnit unit):相当于Object对象中的wait(long timeout)方法。
signal():通知单个线程相当于Object对象中的notify()方法。
signalAll():通知所有线程相当于Object对象中的notifyAll()方法。
4.1 通过ReentrantLock和Condition配合实现单个线程的等待和通知
package com.zs.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class MyService {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
/**
* 等待
*/
public void await(){
try{
lock.lock();
System.out.println("await time: " + System.currentTimeMillis());
condition.await(); // 相当于wait(): 使线程处于等待, 释放锁
System.out.println("condition.await()之后的业务...");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* 唤醒
*/
public void signal(){
try{
lock.lock();
System.out.println("signal time: " + System.currentTimeMillis());
condition.signal(); // 相当于notify(): 唤醒condition上的另一个线程
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
class MyThread extends Thread {
private MyService myService;
public MyThread(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
myService.await();
}
}
public class TestCondition {
public static void main(String[] args) {
try{
MyService myService = new MyService();
MyThread myThread = new MyThread(myService);
// 线程启动之后调用run方法,然后执行方法中的await方法使线程处于等待状态
myThread.start();
// 使当前主线程睡眠,晚点执行
Thread.sleep(5000);
// 调用signal方法,通知MyService中的Condition(监听器)中注册的线程进行唤醒执行
myService.signal();
}catch (Exception e){
e.printStackTrace();
}
}
}
4.2 如果我们要指定通知的下一个进行顺序怎么办呢?
我们可以使用Condition来指定通知进程:
package com.zs.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestCondition {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printA();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printB();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printC();
}
},"C").start();
}
}
class Data {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA() {
lock.lock();
try {
// 业务代码 判断 -> 执行 -> 通知
// 阻塞条件
while (num != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "==> AAAA");
num = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
// 业务代码 判断 -> 执行 -> 通知
while (num != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "==> BBBB");
num = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
// 业务代码 判断 -> 执行 -> 通知
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "==> CCCC");
num = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5 ReentrantLock与公平锁、非公平锁实现
所谓公平锁,就是线程按照执行顺序排成一排,依次获取锁,但是这种方式在高并发的场景下极其损耗性能;这时候,非公平锁应运而生了,所谓非公平锁,就是不管执行顺序,每个线程获取锁的几率都是相同的,获取失败了,才会采用像公平锁那样的方式。这样做的好处是,JVM可以花比较少的时间在线程调度上,更多的时间则是用在执行逻辑代码里面。
Reentrantlock可以实现公平锁和非公平锁:
// Creates an instance of ReentrantLock. This is equivalent to using ReentrantLock(false)
public ReentrantLock() {
sync = new NonfairSync();
}
// Creates an instance of ReentrantLock with the given fairness policy.
// Params: fair – true if this lock should use a fair ordering policy
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
5.1 NonfairSync 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 如果cas尝试获取锁成功(将state锁状态从0设置为1)
if (compareAndSetState(0, 1))
// 设置当前线程为独占锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 如果获取失败返回false 会继续执行将当前线程链入队尾并挂起
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 中断自己
}
如代码所示,在lock的时候,先是尝试将AQS的status从0设为1,成功的话就把当前线程设置为锁的持有者,如果尝试失败了,基于模板方法,实际调用的是Sync的nonfairTryAcquire(int acquires)方法,该方法源码如下:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
if (c == 0) {
//如果当前没有线程在使用,直接使用cas尝试获取锁,新的线程可能抢占已经排队的线程的锁的使用权
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);// 设置当前线程独占锁
return true;
}
}
//如果不为0则判断当前线程是不是独占锁的线程
else if (current == getExclusiveOwnerThread()) {
// 如果是将锁数量状态值+1(可重入锁来源)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//请求即没有获取到锁,也不是当前独占锁的线程,返回false
return false;
}
非公平锁主要就是线程来了之后就进行抢占锁的操作,不管队列中是否有排队的线程与否。
5.2 FairSync 公平锁
公平锁就是每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中获取,下面是FairSync 的源码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
// 如果当前没有线程获取锁,
if (c == 0) {
// !hasQueuedPredecessors()保证线程都按顺序使用锁
// 判断当前线程是否为等待队列中的头结点
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁中能够保证公平的就是这个方法 hasQueuedPredecessors() 来保证线程都按顺序使用锁,就是来判断当前线程是否处于队列中的头结点,如果不是头结点那么就需要排队,如果是头结点就去尝试获取锁。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}