学习笔记:独占锁ReentrantLock的原理

独占锁ReentrantLock的原理

在这里插入图片描述
在这里插入图片描述

ReentrantLock是使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。

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

Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平与公平策略。

abstract static class Sync extends AbstractQueuedSynchronizer {......}
static final class NonfairSync extends Sync {.......}
static final class FairSync extends Sync {........}

在这里,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。
当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者位当前线程。
在该线程没有释放锁的情况下第二次获取该锁,状态值被设置为2,这就是可重入次数。该线程释放锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。

获取锁
1、void lock()
public void lock() {
        sync.lock();
}

ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync。

NonfairSync:非公平锁的实现

final void lock() {
  if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
  else
         acquire(1);
 }

1、默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1:compareAndSetState(0, 1)
2、CAS 成功后,setExclusiveOwnerThread设置该锁持有者是当前线程
3、CAS 失败后,会调用AQS的acquire(1);

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

ReentrantLock重写了tryAcquire(arg)

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

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

实现公平性的核心代码:hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

1、如果当前线程有前驱节点 return true;
2、当前队列为空(h == t)return fasle;
3、h != t 并且 s == null,说明有一个元素将要作为AQS的第一个节点入队,return true;【参考前面AQS的入队过程】
4、h != t && s != null && s.thread != Thread.currentThread()); 说明队列里面的第一个元素不是当前线程 return true;

2、void lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

该方法和lock()方法类似,不同在于:它对中断进行响应。当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
3、boolean tryLock()
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
tryLock()使用的是非公平策略。
注意:该方法不会引起当前线程阻塞。

释放锁
1、void unlock()
public void unlock() {
        sync.release(1);
    }

尝试释放锁
1、如果当前线程持有该锁,则调用该方法会让该线程对线程持有的AQS的状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅是减1而已。
2、如果当前线程没有持有该锁而调用了该方法,则会抛出IllegalMonitorStateException

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的底层是使用AQS实现的可重入独占锁。
  • AQS状态值为0表示当前锁空闲, >=1则说明该锁已经被占用。
  • 该锁内部有公平与非公平实现,默认情况下是非公平的实现
  • 该锁是独占锁,所以某时只有一个线程可以获取该锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
重入(ReentrantLock)是一种独占,也就是说同一时间只能有一个线程持有该。与 synchronized 关键字不同的是,重入可以支持公平和非公平两种模式,而 synchronized 关键字只支持非公平。 重入的实现原理是基于 AQS(AbstractQueuedSynchronizer)框架,利用了 CAS(Compare And Swap)操作和 volatile 关键字。 重入的核心思想是“可重入性”,也就是说如果当前线程已经持有了该,那么它可以重复地获取该而不会被阻塞。在重入内部,使用了一个计数器来记录当前线程持有该的次数。每当该线程获取一次时,计数器就加 1,释放一次时,计数器就减 1,只有当计数器为 0 时,其他线程才有机会获取该。 重入的基本使用方法如下: ```java import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-2").start(); } } ``` 在上面的示例代码中,我们创建了两个线程,分别尝试获取重入。由于重入支持可重入性,因此第二个线程可以成功地获取到该,而不会被阻塞。当第一个线程释放后,第二个线程才会获取到并执行相应的操作。 需要注意的是,使用重入时一定要记得在 finally 块中释放,否则可能会导致死的问题。同时,在获取时也可以设置超时时间,避免由于获取失败而导致的线程阻塞问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值