ReentrantLock重入锁详解

ReentrantLock重入锁

Lock有很多锁的实现,但是直观的实现是ReentrantLock重入锁。

在这里插入图片描述

重入锁的设计目的

比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。

ReentrantLock的使用

public class Demo {

    private static int i = 0;

    private static Lock lock = new ReentrantLock();

    public static void test(){
        lock.lock();
        try{
            i++;
        }finally {
            //一定要在finally中释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < 1000; j++) {
            new Thread(() -> {
               test();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(i);
    }
}

ReentrantReadWriteLock的使用

public class Demo1 {

    static Map<String, Object> cacheMap = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock read = rwl.readLock();
    static Lock write = rwl.writeLock();

    public static final Object get(String key) {
        System.out.println("开始读取数据");
        read.lock(); //读锁
        try {
            return cacheMap.get(key);
        } finally {
            read.unlock();
        }
    }

    public static final Object put(String key, Object value) {
        System.out.println("开始写数据");
        write.lock();//写锁
        try {
            return cacheMap.put(key, value);
        } finally {
            write.unlock();
        }
    }
}

在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性

  • 读锁与读锁可以共享

  • 读锁与写锁不可以共享(排他)

  • 写锁与写锁不可以共享(排他)

ReentrantLock的实现原理

实现锁要考虑的问题

  • 锁的互斥性:共享资源,标记
  • 没有抢占到锁的线程:阻塞,释放CPU资源,唤醒
  • 存储等待线程:队列 FIFO
  • 公平和非公平特性:能否插队
  • 重入特性:同一线程,在持有锁的情况下,再次获取锁,不需要阻塞
CAS 的实现原理
protected final boolean compareAndSetState(int 
expect, int update) {
 // See below for intrinsics setup to support this
 	return unsafe.compareAndSwapInt(this, stateOffset, 			expect, update);
}

通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false.这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。

加锁 lock.lock()

lock.lock()方法有两个实现,公平锁和非公平锁,先看非公平锁NonfairSync.lock()

假设有三个线程ThreadA、ThreadB、ThreadC抢占锁lock

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    final void lock() {
        //通过CAS 设置状态成功,标识当前线程获得锁 ThreadA
        //设置lock实例中 exclusiveOwnerThread为当前线程,state=1
        //然后直接返回,继续执行ThreadA中 lock.lock();之后的代码
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

此时线程ThreadB、ThreadC无法获得锁,进入到acquire(1)方法中

//该方法在AbstractQueuedSynchronizer类中
public final void acquire(int arg) {
    //arg = 1
    //tryAcquire(arg) 会再次尝试获取锁
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //如果传递过来线程中断状态为true 则设置Thread.currentThread().interrupt();
        selfInterrupt();
}

//该方法在ReentrantLock.NonfairSync类中
protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}

//该方法在该方法在ReentrantLock.Sync类中
final boolean nonfairTryAcquire(int acquires) {
    //拿到当前线程 ThreadB
    final Thread current = Thread.currentThread();
    //得到lock的state值,
    //这一步有可能ThreadB进来时,ThreadA没有释放锁 ,而等到ThreadC进来时刚好释放锁,从而会让ThreadC插队
    int c = getState();
    if (c == 0) {
        //若state == 0,则直接获得锁,直接返回,直接执行ThreadB中lock.lock之后的代码
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果state!=0,但是 当前线程是lock中的exclusiveOwnerThread,则是同一线程,属于重入,不阻塞,将state++,然后直接返回
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //以上情况都不是,说明未获得锁,返回false,执行acquireQueued方法 首先将执行addWaiter方法
    return false;
}

//该方法在AbstractQueuedSynchronizer类中
private Node addWaiter(Node mode) {
    //新建一个Node nextWaiter = EXCLUSIVE = null; thread = ThreadB
    Node node = new Node(Thread.currentThread(), mode);
    // 拿到链表尾部元素
    Node pred = tail;
    //如果不为null
    if (pred != null) {
        //设置该节点的上一个节点为尾节点,并且设置该节点为尾节点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果tail为空 或者CAS替换尾部节点失败 进入enq方法
    enq(node);
    return node;
}

private Node enq(final Node node) {
    //传入的node thread = ThreadB nextWaiter = null
    for (;;) {
        //自旋
        Node t = tail;
        //如果尾节点为空 代表链表为空,需要初始化
        if (t == null) { // Must initialize
            //CAS设置head头结点(thread = null) 设置成功后 设置 tail=head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //如果链表已经不为空 设置node的前一个节点为尾节点 且CAS替换尾节点为node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

//添加到等待队列后 执行acquireQueued()方法
//进入到
final boolean acquireQueued(final Node node, int arg) {
    // node thread = ThreadB C线程进来之前可以认为ThreadB为tail
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //自旋
            //获得当前节点的前一个节点 如果为头结点,则进行抢占锁,若抢占成功,则舍弃头结点,将当前节点设置为头结点,且设置thread 由ThreadB变为NULL 返回false
            final Node p = node.predecessor();//由这一行代码可知,进入链表后,获得锁的顺序是固定的
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
     //若当前节点的头一个节点不是头结点 或者没有成功抢占锁 则进入到shouldParkAfterFailedAcquire方法
            //当前一个节点的waitStatus变为waitStatus为SIGNAL后,进入parkAndCheckInterrupt
            //LockSupport.park(this); 阻塞当前线程
            //当线程再次被唤醒以后 会向上传递interrupted状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //如果自旋过程中,如果线程被终止 设置waitStatus为cancel
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //pred 当前节点的前一个节点 node当前节点 出事的waitStatus都是0
    //所以首先会进入到compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    //再次进入时,pred.waitStatus为SIGNAL 直接返回true
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)//Node.SIGNAL = -1
        /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
        return true;
    if (ws > 0) {
        //ws>0 表示为cancel状态 需要移除
        /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
        // cas设置当前节点的前一个节点waitStatus 为SIGNAL 返回到上一个方法后 继续自旋进入
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

stateOffset

一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量

释放锁 lock.unlock()
public final boolean release(int arg) {
    //arg 1
    if (tryRelease(arg)) {
        //tryRelease成功释放锁后 拿到头结点,如果头结点不为空,且h.waitStatus 不为初始值,前面已经讲过,在阻塞之前会设置为SIGNAL -1
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //getState() 一个线程在一次持有锁的过程中 进入同一个共享资源锁定的代码的次数(重入次数+1)
    int c = getState() - releases;
    //如果当前线程不是lock中的ExclusiveOwnerThread 抛出IllegalMonitorStateException
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // c==0 代表当前线程的锁可以释放,进入次数与调用lock.unlock()相同
    //设置lock的ExclusiveOwnerThread 为null 返回true
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //设置lock的state
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    //传进来的时头结点 head
    /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
    int ws = node.waitStatus;
    if (ws < 0)
        //如果头结点的waitStatus 为SIGNAL 则改为0
        compareAndSetWaitStatus(node, ws, 0);

    /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
    // 拿到头结点的下一个节点
    Node s = node.next;
    //如果下一个节点为空 或者是cancel状态,则提出这个节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //如果不为空 则唤醒节点 ThreadB会去竞争资源
        LockSupport.unpark(s.thread);
}

在这里插入图片描述

为什么在释放锁的时候是从 tail 进行扫描

来看一个新的节点是如何加入到链表中的

  1. 将新的节点的 prev 指向 tail
  2. 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线安全性
  3. t.next=node;设置原 tail 的 next 节点指向新的节点

在 cas 操作之后,

t.next=node 操作之前。

存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。

公平锁和非公平锁的区别

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个

  1. 非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会

    //FairSync.tryAcquire
    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;
    }
    
    //这个方法与 nonfairTryAcquire(int acquires)比较,不同的地方在于判断条件多了hasQueuedPredecessors()方法,也就是加入了[同步队列中当前节点是否有前驱节点]的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
    

AQS AbstractQueuedSynchronizer(同步队列)

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。

AQS的两种功能

从使用层面来说,AQS 的功能分为两种:独占和共享

  • 独占锁,每次只能有一个线程持有锁,比如ReentrantLock 就是以独占方式实现的互斥锁
  • 共享锁,允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如ReentrantReadWriteLock的读-读

AQS的内部实现

AQS队列内部维护的是一个FIFO的双向列表,这种结构的特点就是,每个节点有两个指针分别指向前一节点和后一节点。所以双向链表可以很方便的从任意节点访问其前一节点和后一节点。每个Node其实是有Thread封装的,当前程抢占锁失败后,会封装成Node到AQS中。当获取锁的线程释放锁以后,又会从AQS中选一个节点(线程)唤醒。

Node 的组成

在这里插入图片描述

释放锁以及添加线程对于队列的变化

当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。

在这里插入图片描述

这里会涉及到两个变化

  1. 新的线程封装成Node节点追加到同步队列中,设置 prev节点以及修改当前节点的前置节点的next节点指向自己
  2. 通过CAS将tail 重新指向新的尾部节点

head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下
在这里插入图片描述

这个过程也是涉及到两个变化

  1. 修改 head 节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可

整体流程图

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值