• 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();
            }
    
        }
    }
    

    执行结果:

    常用的一些方法

    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 可重入锁的那些事(一)-LMLPHP

    结论二:

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

    2.3.实现锁的可重入特性

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

     ......
     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类只有以下两个方法:

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

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

    ReentrantLock的内部类Sync继承AQS实现模板方法tryRelease(int) 实现锁的释放规则,源码如下👇方法参数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等于锁持有的次数。

    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());
    

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

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

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

    支持哪些数据类型呢?

    3.抽象同步队列AQS

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

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

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

    3.1同步队列

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

    Java 可重入锁的那些事(一)-LMLPHP

    理解节点中的属性值作用

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

    👇图是新增节点的过程

    Java 可重入锁的那些事(一)-LMLPHP

    Java 可重入锁的那些事(一)-LMLPHP

    Java中的线程安全与线程同步

    Java线程状态(生命周期)--一篇入魂

    自己编写平滑加权轮询算法,实现反向代理集群服务的平滑分配

    Java实现平滑加权轮询算法--降权和提权

    Java实现负载均衡算法--轮询和加权轮询

    Java全栈学习路线、学习资源和面试题一条龙

    更多优质文章,请关注WX公众号:Java全栈布道师

    Java 可重入锁的那些事(一)-LMLPHP

    08-19 10:42