【Java 并发笔记】ReentrantLock 相关整理

文前说明

作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。

1. ReentrantLock

  • ReentrantLock 即可重入锁,实现了 Lock 和 Serializable 接口。
  • 在 Java 环境下 ReentrantLocksynchronized 都是可重入锁。
    • 可重入锁,也叫做递归锁,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁。
    • 调用 get() 方法,同一个线程 ID 会被连续输出两次。
public void get() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());
        set();
        lock.unlock();
    }

public void set() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());
        lock.unlock();
}
  • ReentrantLock 构造函数中提供了两种锁:创建公平锁和非公平锁(默认)。
    • RentrantLock 有三个内部类 Sync、NonfairSync 和 FairSync 类。
    • Sync 继承 AbstractQueuedSynchronizer 抽象类。
    • NonfairSync(非公平锁) 继承 Sync 抽象类。
    • FairSync(公平锁) 继承 Sync 抽象类。
public ReentrantLock() {
       sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
}
  • 在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许插队。
    • 当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。
    • 非公平的 ReentrantLock 并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。
  • 在公平的锁上,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。
    • 而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
  • 非公平锁性能高于公平锁性能的原因
    • 在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。
    • 假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于锁被 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此 B 会再次尝试获取这个锁。与此同时,如果线程 C 也请求这个锁,那么 C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面,B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也提高了。
  • 当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。
    • 在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。

1.1 公平锁与非公平锁实现

公平锁

  • 公平锁表示线程获取锁的顺序是按照 线程加锁的顺序 来分配的,即先来先得的 FIFO 先进先出顺序。
    • 每个线程获取锁的过程是公平的,等待时间最长的会最先被唤醒获取锁。
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;
        }
}
  • 公平锁 tryAcquire() 方法比非公平锁 nonfairTryAcquire() 方法多了一个 hasQueuedPredecessors() 方法。
    • 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());
}

非公平锁

  • 而非公平锁就是一种获取锁的抢占机制,是 随机获得锁 的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
    • 非公平锁可能使线程 " 饥饿 "。
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);
        }
}
  • 是否公平只是针对入队前,入队后都是按顺序获取锁。

1.2 重入锁实现

  • 线程可以重复获取已经持有的锁。
    • 在非公平和公平锁中,都对重入锁进行了实现。
if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
}

1.3 使用 ReentrantLock 场景

场景 1:如果发现该操作已经在执行中则不再执行(有状态执行)

private ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {  //如果已经被 lock,则立即返回 false 不会等待,达到忽略操作的效果。 
         try {

            //操作

         } finally {
             lock.unlock();
         }

}

场景 2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似 synchronized)

private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
// private ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
    lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果

   //操作

} finally {
    lock.unlock();
}
  • 公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
  • 不公平情况下,是无序状态允许插队,JVM 会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

场景 3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

try {
     if (lock.tryLock(5, TimeUnit.SECONDS)) {  //如果已经被 lock,尝试等待 5s,看是否可以获得锁,如果 5s 后仍然无法获得锁则返回 false 继续执行
        try {
            //操作
        } finally {
            lock.unlock();
        }
      }
} catch (InterruptedException e) {
      e.printStackTrace(); //当前线程被中断时(interrupt),会抛 InterruptedException                 
}

场景 4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。

try {
    lock.lockInterruptibly();
    //操作

} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}
  • 这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞),该操作的开销也很大,一般不建议使用。

场景 5:条件判断。

  • 每一个 lock 可以有任意数据的 Condition 对象。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
  while(条件判断表达式) {
      condition.wait();
  }
 // 处理逻辑
} finally {
    lock.unlock();
}

1.4 ReentrantLock 的方法

方法 说明
getHoldCount() 查询当前线程获取此锁的次数,此线程执行 lock 方法的次数。
getQueueLength() 返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9。
getWaitQueueLength(Condition condition) 返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了 condition 对象的 await 方法,那么此时执行此方法返回 10。
hasWaiters(Condition condition) 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法。
hasQueuedThread(Thread thread) 查询给定线程是否等待获取此锁。
hasQueuedThreads() 是否有线程等待此锁。
isFair() 该锁是否公平锁。
isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true。
isLock() 此锁是否有任意线程占用。
lockInterruptibly() 如果当前线程未被中断,获取锁。
tryLock() 尝试获得锁,仅在调用时锁未被线程占用,获得锁。
tryLock(long timeout, TimeUnit unit) 如果锁在给定等待时间内没有被另一个线程获取,则获取该锁。

1.5 tryLock、lock 和 lockInterruptibly 的区别

  • tryLock 能获得锁就返回 true,不能就立即返回 false。
    • tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。
  • lock 能获得锁就返回 true,不能的话一直等待获得锁。
  • lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常。

1.6 总结

  • ReentrantLock 提供了内置锁(synchronized)类似的功能和内存语义。
  • ReentrantLock 提供了包括定时的锁等待、可中断的锁等待、公平性以及实现非块结构的加锁。
  • Condition 对线程的等待和唤醒等操作更加灵活,一个 ReentrantLock 可以有多个 Condition 实例,更有扩展性。
  • ReentrantLock 需要显示的获取锁,并在 finally 中释放锁。
  • JDK 1.8 以前 ReentrantLock 在性能上似乎优于 synchronized,但获取锁的操作不能与特定的栈帧关联起来,而内置锁可以。
  • 因为内置锁时 JVM 的内置属性,所以未来更可能提升 synchronized 而不是 ReentrantLock 的性能。
    • 例如对线程封闭的锁对象消除优化,通过增加锁粒度来消除内置锁的同步。

你可能感兴趣的:(【Java 并发笔记】ReentrantLock 相关整理)