Java多线程系列 - ReentranLock

版权声明 :

本文为博主原创文章,如需转载,请注明出处(https://blog.csdn.net/F1004145107/article/details/85163337)

本文大纲

1.ReentrantLock怎么用

2.ReentrantLocksynchronized

3.ReentrantLock进阶使用

常用API

1.ReentrantLock(boolean fair){}

  • 构造器,默认为false非公平锁

  • FairSync公平锁 , NonfairSync非公平锁

  • 公平锁

    • 线程A正在占用锁,线程B正在等待A释放锁,此时线程C来了,C会直接被放到等待线程队列的尾部

  • 非公平锁

    • 如上场景,线程C来了之后会立刻进行争抢锁的操作,抢不到锁才会被加入到等待线程队列的尾部

2.void lock()

  • 上锁,需要先获取当前ReentranLock对象

  • 使用该方法后必须使用unlock进行释放锁,且最好在finally块中执行

public static ReentrantLock lock = new ReentrantLock();
    
    @Override
    public void run() {
        lock.lock();
        try {
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
  • 注意,这里的ReentranLock对象一定要是属于类的,不能是局部变量,否则锁是没有意义的

3.boolean tryLock() / boolean tryLock(long timeout, TimeUnit unit)

  • 尝试重新去获取锁,无论是否成功都会返回boolean值

  • 支持设置重试时间,当时间到了之后还没有获取到锁,就会返回false

4.void lockInterruptibly()

  • lock()一样,都是上锁,但是该方法支持中断,调用Thread.interrupt()方法即可中断该线程

5.boolean isHeldByCurrentThread()

  • 查询当前线程是否持有锁

6. Condition newCondition()

  • 返回Condition对象,该对象方法与Object中的wait,notify等方法功能类似,会在下面有详细的介绍

ReentranLock与synchronized

  • 相同

    • 二者都是可重入锁

      public static ReentrantLock lock = new ReentrantLock();
      ​
          //ReentrantLock
          public void lockMethod1() {
              try {
                  lock.lock();
                  lockMethod2();
              } finally {
                  lock.unlock();
              }
          }
          public void lockMethod2() {
              try {
                  lock.lock();
              } finally {
                  lock.unlock();
              }
          }
      ​
          //synchronized
          public synchronized void method1() {
              method2();
          }
          public synchronized void method2() {
      ​
          }

      如果已经获得了method1的锁,那么就会自动获取method2的锁

    • synchronized在优化之后和ReentranLock一样都是自旋锁,避免了线程进入阻塞状态

  • 区别

    • synchronized是关键字,是在JVM层面上实现的,如果出现了异常情况,JVM会自动释放锁资源,而不会造成死锁

      ReentranLock是java代码实现的,出现异常之后并不会自动释放锁资源,所以必须要在finally代码块中进行锁资源的释放

    • ReentranLock我们可以选择锁的争夺机制,可以选择定时等待的功能,也可以选择在合适的时候进行中断

    • 在资源竞争不激烈的状况下synchronized会更适合我们,它可以提供更为简单的操作,以及更好的可读性,

    在资源竞争激烈的情况下,ReentranLock更有优势一些,因为它可以为我们提供更多更强大的功能,以及更细

    ​ 粒度的操作,方便我们在复杂的环境下进行锁的操作

Condition(进阶应用)

  • 常用API

    // 当前线程进入等待状态,可以被中断
    void await()
    // 当前线程进入等待状态,等到达指定时间后重新抢夺锁,可以被中断
    boolean await(long time, TimeUnit unit)
    // 当前线程进入等待状态,等到达指定时间后重新抢夺锁,如果在时间未到之前被收到signal()
    // 或者signalAll(),则返回指定时间 - 已等待的时间,可以被中断
    long awaitNanos(long nanosTimeout)
    // 当前线程进入等待状态,不可以被中断
    void awaitUninterruptibly()
    // 当前线程进入等待状态,与awaitNanos相似,指定一个等待日期,返回是否到了指定日期
    boolean awaitUntil(Date deadline)
    // 唤醒因为调用await相关方法而进入等待队列中的线程,会唤醒第一个
    void signal()
    // 唤醒因为调用await相关方法而进入等待队列中的线程,会唤醒全部
    void signalAll()
  • Condition中的方法与Object中的方法很相似,那么为什么我们要使用Condition

    • 我们在使用的其实是Condition的一个实现类ConditionObject,ConditionObject是AQS中的一个内部类,所以机制和AQS一样,内部都维护了一个队列,

    • 在调用await()等系列方法的时候其实就是将当前线程加入到了当前ConditionObject中的等待队列中,说到这可能有同学已经知道它的其它用法了,那就是多个ConditionObject来实现线程之间的通讯

    • 我们来看一个例子,有俩个线程,我们要求线程在启动后打印num字段的值,并且要求线程A一直打印1,线程B一直打印0

      /**
       * @author wise
       */
      public class ConditionTest {
      ​
          private static ReentrantLock lock = new ReentrantLock();
          static Condition firstCondition = lock.newCondition();
          static Condition secondCondition = lock.newCondition();
      ​
          private static int num = 0;
      ​
          public static void main(String[] args) {
              ConditionTest test = new ConditionTest();
              Thread threadA = new Thread(new ThreadA(), "线程A");
              Thread threadB = new Thread(new ThreadB(), "线程B");
              threadA.start();
              threadB.start();
          }
      ​
          static class ThreadA extends Thread {
              @Override
              public void run() {
                  lock.lock();
                  try {
                      System.out.println(Thread.currentThread().getName() + "正在运行");
                      while (num == 0) {
                          num++;
                          secondCondition.signal();
                          System.out.println(Thread.currentThread().getName() + "-num : " + num);
                          firstCondition.await();
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      lock.unlock();
                  }
              }
          }
      ​
          static class ThreadB extends Thread {
              @Override
              public void run() {
                  lock.lock();
                  try {
                      System.out.println(Thread.currentThread().getName() + "正在运行");
                      while (num == 1) {
                          firstCondition.signal();
                          num = 0;
                          System.out.println(Thread.currentThread().getName() + "-num : " + num);
                          secondCondition.await();
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      lock.unlock();
                  }
              }
          }
      }

      打印结果如下 :

      线程A-num : 1
      线程B-num : 0
      线程A-num : 1
      线程B-num : 0
      线程A-num : 1
      线程B-num : 0
      线程A-num : 1
      线程B-num : 0
      线程A-num : 1
      线程B-num : 0
      线程A-num : 1

      网上还有一个进阶的例子 :

      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.ReentrantLock;
      ​
      class BoundedBuffer {
          final Lock lock = new ReentrantLock();
          final Condition notFull  = lock.newCondition(); 
          final Condition notEmpty = lock.newCondition(); 
      ​
          final Object[] items = new Object[5];
          int putptr, takeptr, count;
      ​
          public void put(Object x) throws InterruptedException {
              lock.lock();    //获取锁
              try {
                  // 如果“缓冲已满”,则等待;直到“缓冲”不是满的,才将x添加到缓冲中。
                  while (count == items.length)
                      notFull.await();
                  // 将x添加到缓冲中
                  items[putptr] = x; 
                  // 将“put统计数putptr+1”;如果“缓冲已满”,则设putptr为0。
                  if (++putptr == items.length) putptr = 0;
                  // 将“缓冲”数量+1
                  ++count;
                  // 唤醒take线程,因为take线程通过notEmpty.await()等待
                  notEmpty.signal();
      ​
                  // 打印写入的数据
                  System.out.println(Thread.currentThread().getName() + " put  "+ (Integer)x);
              } finally {
                  lock.unlock();    // 释放锁
              }
          }
      ​
          public Object take() throws InterruptedException {
              lock.lock();    //获取锁
              try {
                  // 如果“缓冲为空”,则等待;直到“缓冲”不为空,才将x从缓冲中取出。
                  while (count == 0) 
                      notEmpty.await();
                  // 将x从缓冲中取出
                  Object x = items[takeptr]; 
                  // 将“take统计数takeptr+1”;如果“缓冲为空”,则设takeptr为0。
                  if (++takeptr == items.length) takeptr = 0;
                  // 将“缓冲”数量-1
                  --count;
                  // 唤醒put线程,因为put线程通过notFull.await()等待
                  notFull.signal();
      ​
                  // 打印取出的数据
                  System.out.println(Thread.currentThread().getName() + " take "+ (Integer)x);
                  return x;
              } finally {
                  lock.unlock();    // 释放锁
              }
          } 
      }
      ​
      public class ConditionTest2 {
          private static BoundedBuffer bb = new BoundedBuffer();
      ​
          public static void main(String[] args) {
              // 启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);
              // 启动10个“读线程”,从BoundedBuffer中不断的读数据。
              for (int i=0; i<10; i++) {
                  new PutThread("p"+i, i).start();
                  new TakeThread("t"+i).start();
              }
          }
      ​
          static class PutThread extends Thread {
              private int num;
              public PutThread(String name, int num) {
                  super(name);
                  this.num = num;
              }
              public void run() {
                  try {
                      Thread.sleep(1);    // 线程休眠1ms
                      bb.put(num);        // 向BoundedBuffer中写入数据
                  } catch (InterruptedException e) {
                  }
              }
          }
      ​
          static class TakeThread extends Thread {
              public TakeThread(String name) {
                  super(name);
              }
              public void run() {
                  try {
                      Thread.sleep(10);                    // 线程休眠1ms
                      Integer num = (Integer)bb.take();    // 从BoundedBuffer中取出数据
                  } catch (InterruptedException e) {
                  }
              }
          }
      }

      上面的例子是一个缓存空间中对Condition的运用,通过Condition来保证缓存空间内一定是先存后取

    • 可能有同学会有疑问,为什么我什么也没做,并没有将pX系列线程与notFull绑定,怎么我调用signal()方法时唤醒的就是pX系列线程呢,其实上面已经有讲了,因为在调用await()系列方法时其实就是将当前线程放入到了当前Condition对象中的等待队列中,我们来看一下源码

      //要解决上述疑问其实我们只需要看第二个方法addConditionWaiter()方法即可
      public final void await() throws InterruptedException {
          if (Thread.interrupted())
              throw new InterruptedException();
          Node node = addConditionWaiter();
          int savedState = fullyRelease(node);
          int interruptMode = 0;
          while (!isOnSyncQueue(node)) {
              LockSupport.park(this);
              if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                  break;
          }
          if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
              interruptMode = REINTERRUPT;
          if (node.nextWaiter != null) // clean up if cancelled
              unlinkCancelledWaiters();
          if (interruptMode != 0)
              reportInterruptAfterWait(interruptMode);
      }
      //将当前等待的线程加入到等待队列中
      private Node addConditionWaiter() {
          Node t = lastWaiter;
          // If lastWaiter is cancelled, clean out.
          if (t != null && t.waitStatus != Node.CONDITION) {
              unlinkCancelledWaiters();
              t = lastWaiter;
          }
          //创建当前线程节点
          Node node = new Node(Thread.currentThread(), Node.CONDITION);
          //如果尾部节点为null,那么当前线程节点就是头部节点
          if (t == null)
              firstWaiter = node;
          else   //否则尾部节点的下一个节点就是当前线程节点
              t.nextWaiter = node;
          lastWaiter = node;
          return node;
      }

      剩下的源码如果大家有兴趣可以自行去翻阅

总结

  • 本文是主要关于怎么使用ReentrantLock,以及我们为什么要用ReentrantLock,而不是synchronized,其实如果不是已经确定会出现大量的线程竞争,那么最好还是用synchronized,因为通俗易懂,而且对操作没有很大的要求,以及最后叙述了关于ReentrantLock的进阶应用,希望能对你有所帮助

  • 本文在讲述Condition的时候涉及到了一部分AQS的实现,AQS就是(AbstractQueuedSynchronizer),可以说关于整个Lock最核心的部分就是这里了,本文并没有打算讲的特别深,而且这部分我自己也没有研究的特别深入,如果以后有机会的话我会出相关的博客

原创文章 42 获赞 51 访问量 1万+

猜你喜欢

转载自blog.csdn.net/F1004145107/article/details/85163337