• Java中的可重入锁(2)


    什么是重入锁?

    通常情况下,锁可以用来控制多线程的访问行为。

    那对于同一个线程,如果连续两次对同一把锁进行lock,会怎么样了?

    对于一般的锁来说,这个线程就会被永远卡死在那边,比如:

    void handle() {
        lock();
        lock();  //和上一个lock()操作同一个锁对象,那么这里就永远等待了
        unlock();
        unlock();
    }

    这个特性相当不好用,因为在实际的开发过程中,函数之间的调用关系可能错综复杂,

    一个不小心就可能在多个不同的函数中,反复调用lock(),这样的话,线程就自己和自己卡死了。

      所以,对于希望傻瓜式编程的我们来说,重入锁就是用来解决这个问题的。

    重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死

    因此,如果我们使用的是重入锁,那么上述代码就 可以正常工作。

    你唯一需要保证的,就是unlock()的次数和lock()一样多。这样是不是方便很多呢?

    Java中的重入锁

    Java中的锁都来自与Lock接口,如下图中红框内的,就是重入锁。

     重入锁提供的最重要的方法就是lock()

    • void lock():加锁,如果锁已经被别人占用了,就无限等待。

      这个lock()方法,提供了锁最基本的功能,拿到锁就返回,拿不到就等待。因此,大规模得在复杂场景中使用,是有可能因此死锁的。因此,使用这个方法得非常小心。

    如果要预防可能发生的死锁,可以尝试使用下面这个方法:

    • boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断

      这是一个比单纯lock()更具有工程价值的方法,如果大家阅读过JDK的一些内部代码,就不难发现,tryLock()在JDK内部被大量的使用。

    与lock()相比,tryLock()至少有下面一些好处:

    1. 可以不用进行无限等待。直接打破形成死锁的条件。如果一段时间等不到锁,可以直接放弃,同时释放自己已经得到的资源。这样,就可以在很大程度上,避免死锁的产生因为线程之间出现了一种谦让机制

    2. 可以在应用程序这层进行进行自旋,你可以自己决定尝试几次,或者是放弃

    3. 等待锁的过程中可以响应中断,如果此时,程序正好收到关机信号,中断就会触发,进入中断异常后,线程就可以做一些清理工作,从而防止在终止程序时出现数据写坏,数据丢失等悲催的情况

    当然了,当锁使用完后,千万不要忘记把它释放了。不然,程序可能就会崩溃啦~

    • void unlock() :释放锁
    • public boolean tryLock()

      这个不带任何参数的tryLock()不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false,特别适合在应用层自己对锁进行管理,在应用层进行自旋等待

    重入锁的实现原理

    重入锁内部实现的主要类如下图:

     重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。

    实现重入锁的方法很简单,就是基于一个状态变量state。

    这个变量保存在AbstractQueuedSynchronizer对象中

    private volatile int state;

    当这个state==0时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数

    因此,lock()的最简单的实现是:

     final void lock() {
     // compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
     if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
     else
     //如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待
         acquire(1);
    }

    下面是acquire()  的实现:

     public final void acquire(int arg) {
     //tryAcquire() 再次尝试获取锁,
     //如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
     //同时宣布获得所成功,这正是重入的关键所在
     if (!tryAcquire(arg) &&
         // 如果获取失败,那么就在这里入队等待
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         //如果在等待过程中 被中断了,那么重新把中断标志位设置上
         selfInterrupt();
    }

    公平的重入锁

    默认情况下,重入锁是不公平的。什么叫不公平呢。也就是说,如果有1,2,3,4 这四个线程,按顺序,依次请求锁。

    那等锁可用的时候,谁会先拿到锁呢在非公平情况下,答案是随机的。可能线程3先拿到锁。

    如果你是一个公平主义者,强烈坚持先到先得的话,那么你就需要在构造重入锁的时候,指定这是一个公平锁:

    ReentrantLock fairLock = new ReentrantLock(true);

    这样一来,每一个请求锁的线程,都会乖乖的把自己放入请求队列,而不是上来就进行争抢。

    但一定要注意,公平锁是有代价的。维持公平竞争是以牺牲系统性能为代价的

    如果你愿意承担这个损失,公平锁至少提供了一种普世价值观的实现吧!

    那公平锁和非公平锁实现的核心区别在哪里呢

    来看一下这段lock()的代码:

    //非公平锁 
     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) {
              //上来不管三七二十一,直接抢了再说
              if (compareAndSetState(0, acquires)) {
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          //如果就是当前线程占用了锁,那么就更新一下state,表示重复占用锁的次数
          //这是“重入”的关键所在
          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;
     }

    Condition

    Condition可以理解为重入锁的伴生对象。

    它提供了在重入锁的基础上,进行等待和通知的机制。

    可以使用 newCondition()方法生成一个Condition对象,如下所示。

     private final Lock lock = new ReentrantLock();
     private final Condition condition = lock.newCondition();

    那Condition对象怎么用呢。

    在JDK内部就有一个很好的例子。让我们来看一下ArrayBlockingQueue吧。

    ArrayBlockingQueue是一个队列,你可以把元素塞入队列(enqueue),也可以拿出来take()。

    但是有一个小小的条件,就是如果队列是空的,那么take()就需要等待,一直等到有元素了,再返回。

    那这个功能,怎么实现呢?这就可以使用Condition对象了。

    实现在ArrayBlockingQueue中,就维护一个Condition对象

    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();

    这个notEmpty 就是一个Condition对象。它用来通知其他线程,ArrayBlockingQueue是不是空着的

    当我们需要拿出一个元素时:

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                // 如果队列长度为0,那么就在notEmpty condition上等待了,一直等到有元素进来为止
                // 注意,await()方法,一定是要先获得condition伴生的那个lock,才能用的哦
                notEmpty.await();
            //一旦有人通知我队列里有东西了,我就弹出一个返回
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    当有元素入队时:

       public boolean offer(E e) {
         checkNotNull(e);
         final ReentrantLock lock = this.lock;
         //先拿到锁,拿到锁才能操作对应的Condition对象
         lock.lock();
         try {
             if (count == items.length)
                 return false;
             else {
                 //入队了, 在这个函数里,就会进行notEmpty的通知,通知相关线程,有数据准备好了
                 enqueue(e);
                 return true;
             }
         } finally {
             //释放锁了,等着的那个线程,现在可以去弹出一个元素试试
             lock.unlock();
         }
     }
    
     private void enqueue(E x) {
         final Object[] items = this.items;
         items[putIndex] = x;
         if (++putIndex == items.length)
             putIndex = 0;
         count++;
         //元素已经放好了,通知那个等着拿东西的人吧
         notEmpty.signal();
     }

    因此,整个流程如图所示:

    重入锁的使用示例

    为了让大家更好的理解重入锁的使用方法。现在我们使用重入锁,实现一个简单的计数器。

    这个计数器可以保证在多线程环境中,统计数据的精确性,请看下面示例代码

    public class Counter {
        //重入锁
        private final Lock lock = new ReentrantLock();
        private int count;
        public void incr() {
            // 访问count时,需要加锁
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
        
        public int getCount() {
            //读取数据也需要加锁,才能保证数据的可见性
            lock.lock();
            try {
                return count;
            }finally {
                lock.unlock();
            }
        }
    }

    总结

    对于重入锁,我们需要特别知道几点:

    1. 对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致
    2. 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
    3. 重入锁的内部实现是基于CAS操作的。
    4. 重入锁的伴生对象Condition提供了await()和singal()的功能,可以用于线程间消息通信

    https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

    https://www.jianshu.com/p/f47250702ee7

  • 相关阅读:
    str_split 分隔中文出现乱码 替代函数
    PHP 浮点数 转化 整数方法对比 ceil,floor,round,intval,number_format
    php 判断字符串之间包含关系
    不解之谜
    正则匹配 特殊的 符号
    PHP 判断字符串 是否 包含另一个字符串
    PHP 删除 数组 指定成员
    HTML 权重标签的使用
    【PAT甲级】1094 The Largest Generation (25 分)(DFS)
    【PAT甲级】1093 Count PAT's (25 分)
  • 原文地址:https://www.cnblogs.com/Vincent-yuan/p/15008679.html
Copyright © 2020-2023  润新知