JDK层面提供了Lock锁都是通过Java提供的接口来手动解锁和释放锁的,所以在某种程度上,JDK中提供的Lock锁也叫显示锁、JDK提供的显示锁位于java.util.concurrent.locks包下,Lock接口的源码如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
公平锁的核心就是对争抢锁的所有线程都是公平的,在多线程并发环境下,每个线程在抢占锁的过程中,都会首先检查锁维护的等待队列。如果等待队列为空,当着当前线程是等待队列中的第一个线程,则当前线程会获取到锁,否则,当前线程会加入等待队列的尾部,然后队列中的线程会按照先进先出的规则顺序获取锁资源。
线程抢占公平锁的流程如图所示:
从上图可以看出,公平锁的等待队列中存在线程1,2,3三个线程,并且线程1存在等待队列的头部,线程3存放在等待队列的尾部。
此时,有线程4尝试直接获取公平锁,但线程4在抢占公平锁时,会首先判断锁对应的等待队列是否存在元素。很显然,此时等待队列中存在3个线程。因此,线程4会进入等待队列的尾部,排在线程3的后面。等待队列的线程会按照先进先出的顺序依次出队,获取公平锁。也就是说,线程4会在线程3后面,在等待队列中最后一个出队获取公平锁。
ReentrantLock实现了公平锁机制,在ReentrantLock类中,提供了一个带有boolean类型参数的构造方法,源码如下:
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
在这个构造方中,如果传入的参数为true,就会创建一个FairSync对象赋值给ReentrantLock类的成员变量sync,此时线程获取的锁就是公平锁。fairSync时ReentrantLock类中提供的一个表示公平锁的静态内部类,源码如下:
/**
* Sync object for fair locks
*/
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) {
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;
}
}
可以看出,FairSync类的核心就是调用AQS的模版方法进行线程的入队和出队操作,FairSync类的方法lock()方法会调用AQS的acquire()方法,AQS的acquire()方法会调用tryAcquire()方法,而AQS中的tryAcquire()方法实际上是基于子类实现的,因此,此时调用的还是FairSync类的方法。原因是FairSync类继承了Sync类,而Sync类直接继承了AQS。Sync类是ReentrantLock类中的一个静态抽象内部类,源码如下:
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final 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;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
回到FairSync类中,FairSync类的tryAcquire()方法先通过hasQueuedPredecessors()方法判断队列中是否存在后继结点,如果队列中存在后继结点,并且当前线程未占用锁资源,则tryAcquire()方法会返回false,当前线程会进入等待队列的尾部排队。
ReentrantLock中公平锁的加锁流程中方法调用的逻辑如下图所示:
由上图可以看出,使用ReentrantLock的公平锁,当某个线程调用ReentrantLock的Lock()方法加锁时,会经历如下的加锁流程。
非公平锁的核心就是对抢占锁的所有线程都是不公平的,在多线程并发环境下,每个线程在抢占锁的过程中都会先直接尝试抢占锁,如果抢占锁成功,就继续执行程序的业务逻辑,如果抢占锁失败,就会进入等待队列中排队。
公平锁与非公平锁的核心区别在于对排队的处理上,非公平锁在队列的队首位置可以进行一次插队操作,插队成功就可以获取锁,插队失败就会像公平锁一样进入等待队列排队。在非公平锁环境下,可能会出现某个线程在队列中等待时间过长而一直无法获取锁的现象,这种现象叫做饥饿效应。
虽然非公平锁会产生饥饿效应,但是非公平锁比公平锁性能更优。
线程抢占非公平锁的流程如图:
由上图可以看出,非公平锁对应的等待队列中存在线程1,2,3三个线程。其中线程1在队列的头部,说明线程1已经获取到锁。线程3在队列的尾部。此时线程4尝试获取非公平锁,也就是尝试插入队列的头部。
当线程4插入队列的头部成功时,线程1已经执行完业务逻辑并释放锁,线程4获取到锁,线程3位于等到队列的尾部。
当线程4插入队列的头部失败时,线程2位于队列的头部,线程2会获取到锁。线程4会插入队列的尾部。
ReentrantLock中默认实现的就是非公平锁,例如,调用ReentrantLock的无参构造函数创建的锁对象就是非公平锁,ReentrantLock的无参构造函数源码如下:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
也可以通过ReentrantLock的有参构造函数,传入false来创建非公平锁,源码如下:
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
无论是调用ReentrantLock类的无参构造函数,还是调用ReentrantLock的有参构造函数并传入false,都会创建一个NonfairSync类的对象并赋值给ReentrantLock类的成员变量sync,此时创建的就是非公平锁。
NonfairSync类是ReentrantLock类中的一个静态内部类,源码如下:
/**
* Sync object for non-fair locks
*/
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);
}
}
由上述源码可以看出,在非公平锁的加锁逻辑中,并没有直接将线程放入等待队列的尾部,而是先尝试将当前线程插入等待队列的头部,也就是先尝试获取锁资源。如果获取锁资源成功,则继续执行程序的业务逻辑。如果尝试获取锁资源失败,则调用AQS的acquire()方法获取资源。
同样,NonfairSync类继承了Sync类,而Sync继承了AQS,所以,NonfairSync类在加锁流程的本质上,还是调用了AQS类的模版代码实现入队和出队操作。
AQS的acquire()方法会回调NonfairSync类中的tryAcquire()方法,而在NonfairSync类的tryAcquire()方法中,又会调用Sync类中的nonfairTryAcquire()方法尝试获取锁,nonfairTryAcquire()方法的源码如下:
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看出,在nonfairTryAcquire()方法中,并没有将线程加锁加入等待队列中,只是对锁的状态进行了判断,根据不同的状态进行相应的操作。当锁的状态标识为0时,就直接尝试获取锁,然后执行setExclusiveOwnerThread()方法,不会处理等待队列中的排队节点的逻辑。
ReentrantLock中非公平锁的加锁流程中方法调用的逻辑如下图所示:
由上图可以看出,使用ReentrantLock的非公平锁,当某个线程调用ReentrantLock的lock()方法加锁时,会经历如下的加锁流程。
悲观锁是一种设计思想,顾名思义,它的核心思想就是对于事物保持有悲观的态度,每次都会按照最坏的情况下执行。也就是说,在线程获取数据的时候,总是认为其他的线程会修改数据。所以在线程每次获取数据时都会加锁,在此期间其他线程要想获取相同的数据,则会阻塞,直到获取锁的线程释放锁,当前线程加锁成功后,才能获取到相同的数据。
java提供的synchronized内置锁就是一种悲观锁的实现,而ReentrantLock在一定程度上也是悲观锁的实现,悲观锁存在如下问题。
package com.lifly;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
/**
* @author lifly
* @description
* @date 2023-03-30 22:02
* @versoin 1.0.0
**/
public class PessimismLockTest {
private Lock lock = new ReentrantLock();
public void lockAndUnlock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"抢占锁成功!");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
PessimismLockTest pessimismLockTest = new PessimismLockTest();
IntStream.range(0,5).forEach((i) ->{
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开启抢占锁");
pessimismLockTest.lockAndUnlock();
}).start();
});
}
}
与悲观锁一样,乐观锁也是一种设计思想,其核心思想就是乐观。线程在每次获取数据时,都会认为其他线程不会修改数据,所以不会加锁,但是当前线程在更新数据时会判断当前数据在此期间有没有被其他线程修改过。乐观锁可以使用版本号机制实现,也可以使用CAS机制实现。
乐观锁更适合用于读多写少的场景,可以提高系统的性能。在java中java.util.concurrent.atomic包下原子类,就是基于CAS乐观锁实现的。
package com.lifly;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
/**
* @author lifly
* @description
* @date 2023-03-30 22:08
* @versoin 1.0.0
**/
public class OptimisticLockTest {
private AtomicInteger atomicInteger = new AtomicInteger();
public void incrementCount(){
atomicInteger.incrementAndGet();
}
public int getCount(){
return atomicInteger.get();
}
public static void main(String[] args) throws InterruptedException {
OptimisticLockTest optimisticLockTest = new OptimisticLockTest();
IntStream.range(0,10).forEach((i)->{
new Thread(()->{
optimisticLockTest.incrementCount();
}).start();
});
Thread.sleep(500);
int count = optimisticLockTest.getCount();
System.out.println("最终的结果为:"+count);
}
}
可中断锁指多个线程抢占的过程中可以被中断的锁,不可中断锁指在多个线程抢占的过程中不可以被中断的锁。
Java的JUC包提供的显示锁,如ReentrantLock,就是可中断锁,支持在抢占的过程中中断锁。
在java提供的Lock接口中,有两个方法抛出了InterruptedException异常。如下:
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void lockInterruptibly() throws InterruptedException;
这两个方法在加锁的过程中可以中断锁。具体的逻辑如下:
{
private Lock lock = new ReentrantLock();
public void lockAndUnlock(){
try {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"抢占锁成功");
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"被中断");
}
Thread.sleep(1000);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+"抢占锁被中断");
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
InterruptiblyLockTest interruptiblyLockTest = new InterruptiblyLockTest();
Thread threadA = new Thread(()->{
interruptiblyLockTest.lockAndUnlock();
},"ThreadA");
Thread threadB = new Thread(()->{
interruptiblyLockTest.lockAndUnlock();
},"ThreadB");
threadA.start();
threadB.start();
Thread.sleep(100);
threadA.interrupt();
threadB.interrupt();
Thread.sleep(2000);
}
}
从结果中可以看出,ThreadA抢占锁成功后在休眠的过程中被中断,ThreadB线程在等待加锁时也会被中断,也就是在抢占锁的过程中被中断,线程被中断后,会捕获到InterruptedException异常。说明ReentrantLock中的lockInterruptibly()方法获取的是一种可中断锁。
不可中断锁指线程在抢占锁的过程中不能被中断。也就是说,线程在抢占不可中断锁时,如果抢占成功,则继续执行业务逻辑;如果抢占失败,则阻塞挂起。线程在阻塞挂起的过程中不能被中断。java中的synchronized锁就是不可中断锁。
package com.lifly;
/**
* @author lifly
* @description
* @date 2023-03-30 22:24
* @versoin 1.0.0
**/
public class NonInterruptiblyLockTest {
public synchronized void lock(){
try {
System.out.println(Thread.currentThread().getName()+"抢占锁成功");
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"被中断");
}
Thread.sleep(1000);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+"抢占锁被中断");
}
}
public static void main(String[] args) throws InterruptedException {
NonInterruptiblyLockTest nonInterruptiblyLockTest = new NonInterruptiblyLockTest();
Thread threadA = new Thread(()->{
nonInterruptiblyLockTest.lock();
},"ThreadA");
Thread threadB = new Thread(()->{
nonInterruptiblyLockTest.lock();
},"ThreadB");
threadA.start();
threadB.start();
Thread.sleep(100);
threadA.interrupt();
threadB.interrupt();
Thread.sleep(2000);
}
}
从输出结果中可以看出,无论是threadA还是threadB线程,都是在抢占锁成功后被中断的,在抢占锁的过程中不会被中断,说明synchronized是一种不可中断锁。
排它锁也叫独占锁或互斥锁,排它锁在同一时刻只能被一个线程获取到。某个线程获取到排它锁后,其他线程想再获取同一个资源,只能阻塞等待,直到获取锁的线程释放锁。
java中提供的synchronized锁和ReentrantLock锁都是排它锁的实现。另外,ReadWriteLock中的写锁也是排它锁。
共享锁在同一时刻能够被多个线程获取到。需要注意的是,多个线程同时获取到共享锁后,只能读取临界区的数据,不能修改临界区的数据。也就是说,共享锁时针对读操作的锁。
在java中ReadWriteLock中的读锁,Semaphore类和CountDownLatch类都实现了在同一时刻允许多个线程获取到锁,是共享锁的实现。
可重入锁表示一个线程能够对相同的资源重复加锁。也就是说,同一个线程能够多次进入使用同一个锁修饰的方法或代码块。需要注意的是,在线程释放锁时,释放锁的次数需要与加锁的次数相同,才能保证线程真正释放勒索。例如线程A加锁时执行了3次加锁操作,释放锁就必须执行3次释放锁操作。
在ReentrantLock的内部类Sync的nonfairTryAcquire()方法中,如下代码是ReentrantLock实现可重入锁的关键代码
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
在上述代码中,当前线程已经抢占有锁时,会判断当前线程是否是已经获取锁的线程,如果当前线程是已经获取过锁的线程,则增加内部的状态计数,以此实现锁的可重入性。
当ReentrantLock对象解锁时,会先调用AQS的release()方法,而AQS的release()方法又会调用ReentrantLock的内部类Sync的tryRelease()方法,ReentrantLock的内部类Sync的tryRelease()方法的源码如下
protected final 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;
}
在ReentrantLock的内部类Sync的tryRelease()方法中,首先对状态计数减去传入的值,判断当前线程是否已经获取到锁的线程,如果当前线程时已经获取到锁的线程,直接抛出IllegalMonitorStateException异常。然后定义一个是否成功释放锁的变量free,默认值为false,接下来判断state状态计数的值是否减为0.如果state状态计数的值已经减为0,则说明当前线程已经完全释放锁,此时的锁处于空闲状态,将是否成功释放锁的变量free设置为0,并将当前拥有锁的线程置为null。最后,设置锁的状态标识,返回free,结果会返回true。
如果state状态计数的值没有减为0,则说明当前线程并没有完全释放锁,此时的free变量为false,返回free,结果会返回false.
所以,在ReentrantLock中,可重入锁的加锁操作会累加状态计数,解锁操作会累减状态计数。
java中的synchronized锁和ReentrantLock锁都实现了可重入性。
package com.lifly;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
/**
* @author lifly
* @description
* @date 2023-04-05 19:50
* @versoin 1.0.0
**/
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
/**
* 加锁并释放锁
*/
public void lockAndUnlock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"第一次抢占锁成功");
lock.lock();
System.out.println(Thread.currentThread().getName()+"第二次抢占锁成功");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"第一次释放锁成功");
lock.unlock();
System.out.println(Thread.currentThread().getName()+"第二次释放锁成功");
}
}
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
IntStream.range(0,2).forEach((i)->{
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开启抢占锁");
reentrantLockTest.lockAndUnlock();
}).start();
});
}
}
上述案例的输出结果说明了线程在获取可重入锁,释放锁成功的次数与加锁成功的次数相同时,才能完全释放锁,其他线程才能获取到相同的锁。
synchronized锁实现可重入锁源代码:
package com.lifly;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
/**
* @author lifly
* @description
* @date 2023-04-05 20:04
* @versoin 1.0.0
**/
public class SynchronizedLockTest {
/**
* 加锁并释放锁
*/
public void lockAndUnlock(){
synchronized (this){
System.out.println(Thread.currentThread().getName()+"第一次抢占锁成功");
synchronized (this){
System.out.println(Thread.currentThread().getName()+"第二次抢占锁成功");
}
System.out.println(Thread.currentThread().getName()+"第一次释放锁成功");
}
System.out.println(Thread.currentThread().getName()+"第二次释放锁成功");
}
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
IntStream.range(0,2).forEach((i)->{
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开启抢占锁");
reentrantLockTest.lockAndUnlock();
}).start();
});
}
}