本文主要包含的内容:可重入锁(ReedtrantLock)、公平锁、非公平锁、可重入性、同步队列、CAS等概念的理解

显式锁:lock:

上一篇文章提到的synchronized关键字为隐式锁,会自动获取和自动释放的锁,而相对的显式锁则需要在编程时指明何时获取锁,何时释放锁。

通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都需要先获取锁;而有一些锁可能允许并发访问共享资源。

本文主要讲解可重入锁(ReentrantLock),该锁为独占共享资源锁,即独占锁。

1.可重入锁(ReentrantLock)

可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码,又因该锁通过线程独占共享资源的方式确保并发安全,又称为 独占锁 。

举个例子:同一个类中的synchronize关键字修饰了不同的方法。synchronize是内置的隐式的可重入锁,例子中的两个方法使用的是同一把锁,只要能执行testB()也就说明线程拿到了锁,所以执行testA()方法就不用被阻塞等待获取锁了;如果不是同一把锁或非可重入锁,就会在执行testA()时被阻塞等待。


public class Demo {

    public synchronized void testA(){
        System.out.println("执行测试A");
    }

    public synchronized void testB(){
        System.out.println("执行测试B");
        testA();
    }

}


1.1.可重入锁的类图关系

ReentrantLock实现了 Lock 接口和 Serializable 接口(都没画出来),它有三个内部类( Sync 、 NonfairSync 、 FairSync ), Sync 是一个抽象类,它继承  AbstractQueuedSynchronizer 抽象同步队列 ,同时有两个实现类( NonfairSync 和 FairSync ),其中父类 AQS 是个模板类提供了许多以锁相关的操作,子类分别是两种不同的获取锁实现( 非公平锁和公平锁 )。AQS 又继承了 AbstractOwnableSynchronizer 类, AOS 用于保存锁被 独占 的线程对象。

java 可重入 java可重入锁有哪些_servlet

ReentrantLock 类的构造方法有如下两种,很显然,在对象实例化时将决定同步器Sync是公平还是非公平。

// ReentrantLock类

private final Sync sync;
// 默认非公平
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}


先关注ReentrantLock类的方法lock() 和 unlock()。从源码可以发现 ReentrantLock类的方法是交给内部类Sync 类来实现 ,而lock()方法在Sync类中是个抽象方法,具体实现在子类FairSync和NonfairSync类。其实ReentrantLock类中的其他方法也是交给Sync类去处理的,所以想要理解ReentrantLock类的重点是理解Sync类。

注意一个点:Sync类中lock()抽象方法不是Lock接口的抽象方法,它们是通过调用(如下:point_down:)代码产生关联的。


// java.util.concurrent.locks.ReentrantLock类

public void lock() {
    sync.lock();
}
public void unlock() {
    sync.release(1);
}


结论一:

  • ReentrantLock 可重入锁 获取锁 有两种实现:公平和非公平;注意:从类图关系我们可以知道,公平和非公平内部类只有两个方法,都是与获取锁有关,公平与否仅针对获取锁而言,也即是lock()方法。PS:tryAcquire(int)最终会被lock()调用。
  • ReentrantLock的理解重点源码应该关注内部同步器Sync类和Sync的父类抽象同步队列AbstractQueuedSynchronizer。

1.2.怎么使用ReentrantLock

使用案例:并发安全访问共享资源


public class LockDemo {
    public static void main(String[] args) {
        // 简单模拟20人抢优惠
        for(int i=0;i<20;i++){
            new Thread(new ThreadDemo()).start();
        }
    }

}
// 前十位可以获取优惠,凭号码兑换优惠
class ThreadDemo implements Runnable{
    private static Integer num = 10;
    private static final ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 获取锁
        reentrantLock.lock();
        try {
            if(num<=0){
                System.out.println("已被抢完,下次再来");
                return;
            }
            System.out.println(Thread.currentThread().getName()+"用户抢到的号码:"+num--);
        }finally {
            // 释放锁
            reentrantLock.unlock();
        }

    }
}


执行结果:

Thread-18用户抢到的号码:10

Thread-14用户抢到的号码:9

Thread-15用户抢到的号码:8

Thread-4用户抢到的号码:7

Thread-1用户抢到的号码:6

Thread-19用户抢到的号码:5

Thread-11用户抢到的号码:4

Thread-17用户抢到的号码:3

Thread-16用户抢到的号码:2

Thread-13用户抢到的号码:1

已被抢完,下次再来

已被抢完,下次再来

……

常用的一些方法

方法名称

描述

void lock()

获取锁

boolean tryLock()

尝试获取锁,调用该方法不会阻塞,会立即返回获取结果,获取到则返回true,获取不到则返回false

boolean tryLock(long timeout, TimeUnit unit)

尝试在阻塞的指定时间内获取锁

void lockInterruptibly()

获取锁,除非当前线程是interrupted,即发生中断时,结束锁的获取

void unlock()

释放锁

boolean isHeldByCurrentThread()

查询此锁是否由 当前 线程持有

boolean isLocked()

查询此锁是否由 任何 线程持有

2.一些概念的理解

2.1.锁和同步队列的关系

前面讲述过:ReentrantLock类的方法都是交给内部类Sync类来实现的。

Sync和它的子类都实现了,为什么还要ReentrantLock类来套这么一层呢?这关系到锁的使用和实现的问题。

  • 锁是面向开发者,隐藏细节让锁的开发变得更简洁;
  • 抽象同步队列是面向锁的实现,屏蔽了同步状态的管理、线程的排队、等待与唤醒等底层操作,简化了自定义同步器和锁的实现。

说白了,ReentrantLock(锁)类为了简化开发者的使用,具体实现交由其内部类自定义的同步器Sync去处理,而AQS则以模板的方式提供一系列有关锁的操作及部分可被子类Sync重写的模板方法。

2.2.公平锁与非公平锁概述

公平与非公平指的是获取锁的机制不同。

公平锁强调先来后到,表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定,即同步队列记录线程先后顺序,队列的特性FIFO(先进先出);

非公平锁只要CAS设置同步状态成功,当前线程就会获取到锁,没获取成功的依然放在同步队列中按FIFO原则等待,等待下一次的CAS操作。

从源码上可以知道它们的主要区别是多一个判断: !hasQueuedPredecessors()

该判断表示:加入了同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而 非公平锁是没有这个判断的 。


// java.util.concurrent.locks.ReentrantLock.NonfairSync
// 非公平
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);

}
// java.util.concurrent.locks.ReentrantLock.Sync
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;
}

// java.util.concurrent.locks.ReentrantLock.FairSync
// 公平:比非公平多了一步判断 !hasQueuedPredecessors()
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;
}


附上 获取锁时 公平锁和非公平锁的源码区别图

java 可重入 java可重入锁有哪些_java_02

结论二:

公平锁和非公平锁的主要区别是: !hasQueuedPredecessors() ,表示同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而 非公平锁没有这个判断 。

2.3.实现锁的可重入特性

前面在 公平锁与非公平锁概述 这点中,附上了对比两者的关键源码,其中可重入的源码是一样的:point_down:

......
 else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0)
        throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
}

判断当前线程和当前拥有独占访问权限的线程对比,是同一个线程则可以重新进入同一把锁。处理逻辑是:对同步状态state加上acquires=1,然后返回true,返回true即获取锁成功。

AbstractOwnableSynchronizer类用于保存 锁被独占的线程对象 ,AOS类只有以下两个方法:

  • Thread getExclusiveOwnerThread()为获取当前拥有独占访问权限的线程,
  • void setExclusiveOwnerThread(Thread)为设置当前拥有独占访问权限的线程。

所以每次在获取锁成功后会做这么一步: setExclusiveOwnerThread(current) :point_down:


if (compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}


ReentrantLock的内部类Sync继承AQS实现模板方法 tryRelease(int) 实现锁的释放规则,源码如下:point_down:方法参数releases=1。

先判断该线程是否为当前拥有独占访问权限的线程,再判断同步状态,如果状态不为0,则锁还没释放完,不执行 setExclusiveOwnerThread(null) 即不释放独占访问权限的线程。因为发生锁的重入时,同步状态state>1,所以锁释放时同步状态需要一层层出来,直到同步状态为0时,才会置空拥有独占访问权的线程。因此AQS的state状态表示锁的持有次数。


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;
}


结论三:公平和非公平的可重入性都一样,并且同步状态state的作用如下

  • 同步状态state<0 表示 throw new Error("Maximum lock count exceeded");
  • 同步状态state=0 表示锁没有被占用
  • 同步状态state=1 表示锁被占用了
  • 同步状态state>1 表示锁发生了重新进入

即同步状态state等于锁持有的次数。

2.4.CAS概述

CAS的全称是Compare And Swap,意思是 比较并交换 ,是一种特殊的处理器指令。

以方法compareAndSetState(int expect,int update)为例:

处理逻辑是:期望参数expect值跟内存中当前状态值 比较 ,等于则 原子性的修改 state值为update参数值。

获取锁操作:compareAndSetState(0, 1),当同步状态state=0时,则修改同步状态state=1

compareAndSetState() 方法调用了Unsafe 类下的本地方法compareAndSwapInt(),该方法由JVM实现CAS一组汇编指令,指令的执行必须是连续的不可被中断的,不会造成所谓的数据不一致问题,但只能保证一个共享变量的 原子性操作 。

同步队列中还有很多CAS相关方法,比如:

compareAndSetWaitStatus(Node,int,int):等待状态的原子性修改

compareAndSetHead(Node):设置头节点的原子性操作

compareAndSetTail(Node, Node):从尾部插入新节点的原子性操作

compareAndSetNext(Node,Node,Node):设置下一个节点的原子性操作

除了同步队列中提供的CAS方法,在Java并发开发包中,还提供了一系列的CAS操作,我们可以使用其中的功能让并发编程变得更高效和更简洁。

java.util.concurrent.atomic 一个小型工具包,支持 单个变量 上的无锁线程安全编程。

比如:num++ 或num--,自增和自减这些操作是非原子性操作的,无法确保线程安全,为了提高性能不考虑使用锁(synchronized、Lock),可以使用AtomicInteger类的方法来完成自增、自减,其本质是CAS原子性操作。


AtomicInteger num = new AtomicInteger(10);
// 自增
System.out.println(num.getAndIncrement());
// 自减
System.out.println(num.getAndDecrement());


注意:只是在自增和自减的过程是原子性操作。

如下代码:point_down:下面整块代码是非线程安全的,只是 num.getAndDecrement() 自减时是原子性操作,也即是并发场景下num.get()无法确保获取到最新值。


private static AtomicInteger num = new AtomicInteger(10);
......
if(num.get()<=0){
    System.out.println("已被抢完,下次再来");
    return;
}
System.out.println("号码:"+num.getAndDecrement());


支持哪些数据类型呢?

基本数据类型

数组类型

引用类型

更新类型中的字段

  • AtomicBoolean:原子更新布尔值类型
  • AtomicInteger:原子更新整数类型
  • AtomicLong:原子更新长整型
  • AtomicIntegerArray:原子更新整型数组里的元素
  • AtomicLongArray:原子更新长整型数组里的元素
  • AtomicReferenceArray:原子更新引用类型数组里的元素
  • AtomicReference:原子更新引用类型
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

3.抽象同步队列AQS

AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它。AQS继承了 AbstractOwnableSynchronizer 类,AOS用于保存线程对象,保存什么线程对象呢? 保存锁被独占的线程对象 。

抽象同步队列AQS除了实现序列化标记接口,并没有实现任何的同步接口,该类提供了许多同步状态获取和释放的方法给自定义同步器使用,如ReentrantLock的内部类Sync。抽象同步队列支持独占式或共享式的的获取同步状态,方便实现不同类型的自定义同步器。一般方法名带有 Shared 的为共享式,比如,尝试以共享式的获取锁的方法 int tryAcquireShared(int) ,而独占式获取锁方法为 boolean tryAcquire(int) 。

AQS是抽象同步队列,其重点就是 同步队列 及 如何操作同步队列 。

3.1同步队列

双向同步队列,采用尾插法新增节点,从头部的下一个节点获取操作节点,节点自旋获取同步锁,实现FIFO(先进先出)原则。

java 可重入 java可重入锁有哪些_java 可重入_03

理解节点中的属性值作用

  • prev:前驱节点;即当前节点的前一个节点,之所以叫前驱节点,是因为前一个节点在使用完锁之后会解除后一个节点的阻塞状态;
  • next:后继节点;即当前节点的后一个节点,之所以叫后继节点,是因为“后继有人”了,表示有“下一代”节点承接这个独有的锁:lock:;
  • nextWaiter:表示指向下一个 Node.CONDITION 状态的节点(本文不讲述Condition队列,在此可以忽略它);
  • thread:节点对象中保存的线程对象,节点都是配角,线程才是主角;
  • waitStatus:当前节点在队列中的等待状态

因篇幅原因,关于抽象同步队列AQS、锁的获取过程、锁的释放过程、自旋锁、线程阻塞与释放、线程中断与阻塞关系等内容将在下一篇文章展开讲解。

:point_down:图是新增节点的过程

java 可重入 java可重入锁有哪些_公平锁_04