锁主要用来控制多线程访问的行为,对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,这样的特性很不好,在实际的开发中,方法之间的调用方式错综复杂,如果不小心可能在多个不同的方法中,反复调用 lock(),这样就会把自己卡死。
所以,重入锁就是用来解决这个问题的,重入锁使同一个线程可以对同一把锁在不释放的前提下,反复的加锁不会导致线程的卡死,唯一的一点就是需要保证 unlock() 的次数和 lock()一样的多。
重入锁的实现Java中的锁都来自与 Lock 接口,而 ReadWriteLoc k实现的 lock 接口,本文主要分析这两个接口的几个子类的实现细节。
而重入锁最重要的方法就是lock()。
- lock():加锁,如果锁已经被别人占用了,就无限等待
- tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断
- unlock() :释放锁
- tryLock():不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false
- lockInterruptibly():可以响应中断,lock方法会阻塞线程直到获取到锁
- newCondition():返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。
重入锁实现的主要类如下图:
重入锁的核心功能委托给内部类 Sync 实现,并且根据是否是公平锁有 FairSync 和 NonfairSync 两种实现。这是一种典型的策略模式。
实现重入锁的方法很简单,就是基于一个状态变量 state。这个变量保存在AbstractQueuedSynchronizer对象中
private volatile int state;
当 stat e== 0 时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock() 的最简单的实现是:
final void lock() { // CAS设置共享状态,返回true表示成功获取共享状态 if (compareAndSetState(0, 1)) // 设置当前线程为共享状态的持有线程 setExclusiveOwnerThread(Thread.currentThread()); else // 否则调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态 acquire(1); }
下面是acquire() 的实现:
public final void acquire(int arg) { //tryAcquire() 再次尝试获取锁, //如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数, //同时宣布获得所成功,这正是重入的关键所在 if (!tryAcquire(arg) && // 如果获取失败,那么就在这里入队等待 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果在等待过程中 被中断了,那么重新把中断标志位设置上 selfInterrupt(); }
下面我们说一下公平锁 和 非公平锁
公平的重入锁初始化时, state = 0,表示无人抢占了锁。这时候,这时线程 A 请求锁,获得了锁,把 state + 1,如下所示:
线程 A 取得了锁,把 state +1,这时候 state 改为 1,线程 A 继续执行其他任务,此时线程B请求锁,线程 B 无法获取锁,生成节点进行排队,如下图所示:
初始化的时候,会生成一个空的头节点,然后才是线程 B 节点,这时候,如果线程 A 又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当 A 再次请求锁,这时候的状态如下图所示:
到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程 A 释放了一次锁,如下图所示:
仅仅是把状态值减了,只有线程 A 把此锁全部释放了,状态值减到 0 了,其他线程才有机会获取锁。当线程 A 把锁完全释放后,state 恢复为 0,然后会通知队列唤醒线程 B 节点,使B可以再次竞争锁。当然,如果线程 B 后面还有线程 C,线程 C 继续休眠,除非 B 执行完了,通知了线程 C。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。
理解了公平锁的话,那非公平锁就容易理解了,当线程 A 执行完之后,要唤醒线程 B 是需要时间的,而且线程 B 醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程 C,那么线程 C 是有可能获取到锁的,如果线程 C 获取到了锁,线程 B 就只能继续等待休眠了。
那公平锁和非公平锁实现的核心区别在哪里呢?如下所示:
//非公平锁 final void lock() { //直接抢了再说 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else //抢不到,就进队列慢慢等着 acquire(1); } //公平锁 final void lock() { //直接进队列等着 acquire(1); }
我们从代码中可以看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。
而对应tryLock()方法也非常类似的:
//非公平锁 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 如果当前共享状态未被其他线程占用 if (c == 0) { // 尝试通过CAS占有当前共享状态 if (compareAndSetState(0, acquires)) { // 设置共享状态持有线程为当前线程 setExclusiveOwnerThread(current); return true; } } // 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程 else if (current == getExclusiveOwnerThread()) { // 如果是则自增获取次数,设值state int nextc = c + acquires; if (nextc < 0) 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(); // 如果同步状态为0,则说明当前同步状态已完全释放 if (c == 0) { // 1、hasQueuedPredecessors判断当前节点是否存在前驱节点 // 2、如果不存在则CAS设置state的值 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; }引入 Condition
Condition 的作用与 Object.wait() 和 Object.notify() 的作用大致是相同的。但是 wait() 和 notify() 方法是与synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。通过 Lock 接口(重入锁实现了这一接口)的new Condition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,可以让线程在合适的时间等待,或者在某一个特定的时间得到通知。
Condition 接口提供的方法
void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求 void awaitUninterrruptibly();
//调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时 //其中 nanosTimeout 指的等待超时时间,单位纳秒 long awaitNanos(long nanosTimeout) throws InterruptedException;
//同 awaitNanos,但可以指明时间单位 boolean await(long time, TmeUnit unit) throws InterruptedException;
//调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时 //间期限(deadline),如果没到指定时间就被唤醒,返回 true,其他情况返回 false boolean await(Date deadline) throws InterruptedException;
//唤醒一个等待在 Condition 上的线程,该线程从等待方法返回前必须 //获取与 Condition 相关联的锁,功能与notify()相同 void signal();
//唤醒所有等待在 Condition 上的线程,该线程从等待方法返回前必须 //获取与 Condition 相关联的锁,功能与 notifyAll() 相同 void signalAll();
代码演示一下 Condition 的使用:
public class ReentrantLockCondition implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); //通过 ReentrantLock 创建 Condition 实例,并与之关联 public static Condition condition = lock.newCondition(); @Override public void run() { try { lock.lock(); condition.await(); System.out.println("Thread is going on"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { // TODO Auto-generated method stub ReentrantLockCondition condition1 = new ReentrantLockCondition(); Thread thread= new Thread(condition1 ); thread.start(); Thread.sleep(2000); lock.lock(); condition.signal(); lock.unlock(); } }
与 Object.wait() 和 Object.notify() 方法类似,当前线程使用 Condition.await() 时,要求线程持有相关的重入锁,在Condition.await() 调用后,这个线程会释放这把锁。同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。在 signal() 方法调用后,系统会从当前 Condtion 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal() 方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。
总结对于重入锁,这里我们需要知道几点:
- 对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致
- 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
- 重入锁的内部实现是基于 CAS *** 作的
- 重入锁的伴生对象 Condition 提供了 await() 和 singal() 的功能,可以用于线程间消息通信
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)