概念
在Java中,“lock”(锁)是一种用于并发控制的机制。它用于确保在多线程环境中,同一时刻只有一个线程可以访问共享资源或临界区。当一个线程获得了锁,其他线程将被阻塞,直到持有锁的线程释放它。这样可以避免多个线程同时访问共享资源而引发的数据竞争和不确定行为。
lock
是一个接口,而synchronized
是在JVM
层面实现的。synchronized
释放锁有两种方式:
- 获取锁的线程执行完同步代码,释放锁 。
- 线程执行发生异常,
jvm
会让线程释放锁。
lock
锁的释放,出现异常时必须在finally
中释放锁,不然容易造成线程死锁。lock
显式获取锁和释放锁,提供超时获取锁、可中断地获取锁。
synchronized
是以隐式地获取和释放锁,synchronized
无法中断一个正在等待获取锁的线程。
synchronized
原始采用的是CPU
悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU
转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU
频繁的上下文切换导致效率很低。
Lock
用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS
操作。
lock的接口中,主要的方法如下:
public interface Lock {
// 加锁
void lock();
// 尝试获取锁
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解锁
void unlock();
}
ReentrantLock
ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。
public class ReentrantLock implements Lock, java.io.Serializable
类的内部类
ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。
说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
Sync类
在ReentrantLock
中,Sync
是一个重要的内部类,它是ReentrantLock
的静态内部类,并继承自AbstractQueuedSynchronizer
(AQS)。Sync
类是ReentrantLock
实现可重入锁的核心。通过继承AQS,Sync
可以利用AQS提供的底层同步器框架来实现独占锁的语义。
下面对ReentrantLock.Sync
进行详细讲解:
static final class Sync extends AbstractQueuedSynchronizer {
// 获取锁 -》之后继承的非公平锁和公平锁需要实现方法
abstract void lock();
// 非公平方式获取
protected boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当前锁没有被占用,可以尝试获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 使用CAS尝试获取锁
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;
}
// 重写AQS的释放锁方法
protected boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 非锁持有线程尝试释放锁,抛出异常
boolean free = false;
if (c == 0) {
free = true; // 锁的计数归零,可以释放锁
setExclusiveOwnerThread(null); // 清空锁的持有者
}
setState(c); // 更新锁的计数
return free; // 返回是否释放锁成功
}
}
主要方法解释:
nonfairTryAcquire(int acquires)
:用于非公平地尝试获取锁。该方法用于尝试非公平地获取锁。非公平锁的特点是,当有多个线程同时请求锁时,不会按照线程的请求顺序来获取锁,而是直接尝试获取锁,如果能获取成功,则立即获得锁,即使其他线程在等待。。首先,它获取当前线程,然后尝试获取锁的状态。如果锁的状态为0,说明当前锁没有被占用,那么当前线程尝试使用CAS操作获取锁。如果成功获取锁,将当前线程设置为独占锁的持有者,然后返回true
表示获取锁成功。如果当前线程已经持有锁(可重入锁),则直接增加锁的计数,并返回true
表示获取锁成功。如果获取锁失败,则返回false
。
tryRelease(int releases)
:这是重写的AQS方法,用于释放锁。首先,它会减少锁的计数。如果当前线程不是锁的持有者,说明该线程没有持有锁,尝试释放锁会抛出IllegalMonitorStateException
异常。如果锁的计数归零,说明锁可以完全释放,此时清空锁的持有者,并返回true
表示释放锁成功。否则,返回false
表示锁的计数还未归零,锁并未完全释放。
ReentrantLock.Sync
类的主要功能是根据不同情况判断是否能够获取锁,并且支持锁的重入机制。在ReentrantLock
实例中,Sync
实例会被创建并持有,用于实现锁的功能。
FairSyn类-公平锁
公平锁(Fair Lock): 公平锁的获取策略是按照线程请求锁的顺序来分配锁。当有多个线程等待锁时,锁会选择最早等待的线程来获取锁。这种策略确保所有等待锁的线程都有公平的机会获得锁,避免了线程饥饿的情况。但是,由于要维护等待队列的顺序,公平锁的性能可能较非公平锁稍低。
FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下:
// 公平锁
static final class FairSync extends Sync {
// 版本序列化
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 以独占模式获取对象,忽略中断
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 尝试公平获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
if (c == 0) { // 状态为0
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
// 设置当前线程独占
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
// 下一个状态
int nextc = c + acquires;
if (nextc < 0) // 超过了int的表示范围
throw new Error("Maximum lock count exceeded");
// 设置状态
setState(nextc);
return true;
}
return false;
}
}
说明: 跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。
说明: 可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部.
使用示例-(构造函数-默认非公平锁)
多次运行代码发现,重入锁默认采用的是非公平锁
package com.hhee.oa.reimbursement.entity;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建5个线程并启动
for (int i = 1; i <= 5; i++) {
Thread thread = new Thread(new Worker(), "Thread-" + i);
thread.start();
}
}
static class Worker implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 2; i++) {
// 在每次迭代中,获取锁、执行任务、释放锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "执行" + i);
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放" + i);
}
}
}
}
}
结果
Thread-1执行1
Thread-1释放1
Thread-2执行1
Thread-2释放1
Thread-4执行1
Thread-4释放1
Thread-5执行1
Thread-5释放1
Thread-3执行1
Thread-3释放1
Thread-1执行2
Thread-1释放2
Thread-2执行2
Thread-2释放2
Thread-4执行2
Thread-4释放2
Thread-5执行2
Thread-5释放2
Thread-3执行2
Thread-3释放2
源码位置
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
NonfairSync类-非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
跳转到Sync默认的方法nonfairTryAcquire,参考Sync内部方法说明。