Lock与Condition:解决并发的万能钥匙

Lock与Condition:解决并发的万能钥匙,第1张

Lock与Condition:解决并发的万能钥匙

Lock与Condition:解决Java并发的万能钥匙
    • 为什么再造管程
    • Lock如何保证可见性
    • 什么是可重入锁
    • 公平与非公平锁
    • 锁的最佳实践
    • Condition条件变量
    • Condition在Dubbo中的应用

为什么再造管程

在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?

你也许经常看到别人这样说,在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。如果仅仅是因为性能的限制,那么优化一下即可,大可不必再重新造一个管程。

在破解死锁问题的解决方案中,有一个破坏不可抢占条件方案,但是这个
方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不
到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占
有的资源。但我们期望对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

如果重新设计一把锁机解决synchronized不能破解死锁不可抢占的问题,你会从哪几个方面进行考虑呢?

  • 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么
    线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞
    状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能
    够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
  • 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个
    错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  • 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线
    程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

Java SDK并发包的Lock方法从上面三个角度进行了实现,对应到API的三个方法。

// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
Lock如何保证可见性

Java 里多线程的可见性是通过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?

举个例子:线程 T1 对value 进行了 +=1 *** 作,那后续的线程 T2 能够看到 value 的正确结果吗?

class X {
  private final Lock rtl = new ReentrantLock();
  int value;
  public void addOne() {
  // 获取锁
  rtl.lock();
  try {
    value+=1;
  } finally {
  // 保证锁能释放
  rtl.unlock();
  }
 }
}

Java SDK 里面锁的实现非常复杂,这里我就不展开细说了,但是原理还是需要简单介绍一下:它是利用了 volatile 相关的 Happens-Before 规则。Java SDK里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写state 的值;解锁的时候,也会读写 state 的值。

根据相关的 Happens-Before 规则:

  • 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的 *** 作 unlock();
  • volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() *** 作
    Happens-Before 线程 T2 的 lock() *** 作;
  • 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() *** 作。
什么是可重入锁

我们创建的锁的具体类名是 ReentrantLock,这个翻译过来叫可重入锁,所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。

例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在
① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁 *** 作。此时,如果锁 rtl 是可重入
的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻
塞。

class X {
   private final Lock rtl = new ReentrantLock();
   int value;
   public int get() {
   // 获取锁
   rtl.lock(); ②
   try {
     return value;
   } finally {
   // 保证锁能释放
   rtl.unlock();
  }
 }
  public void addOne() {
  // 获取锁
  rtl.lock();
  try {
   value = 1 + get(); ①
  } finally {
  // 保证锁能释放
  rtl.unlock();
  }
 }
}
公平与非公平锁

ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。

// 无参构造函数:默认非公平锁
public ReentrantLock() {
  sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
  sync = fair ? new FairSync() : new NonfairSync();
}

锁都对应着一个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。

锁的最佳实践

在此引入并发大师Doug Lea的最佳实践总结:

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁
Condition条件变量

Java 语言内置的管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,这是二者的一个重要区别。

例如,实现一个阻塞队列,就需要两个条件变量:
一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),另一个是队列
不满(队列已满不允许入队)。

public class BlockedQueue{
   final Lock lock = new ReentrantLock();
   // 条件变量:队列不满
   final Condition notFull = lock.newCondition();
   // 条件变量:队列不空
   final Condition notEmpty = lock.newCondition();
   // 入队
   void enq(T x) {
     lock.lock();
     try {
       while (队列已满){
        // 等待队列不满
        notFull.await();
      }
       // 省略入队 *** 作...
       // 入队后, 通知可出队
       notEmpty.signal();
     }finally {
      lock.unlock();
     }
   }
   // 出队
   void deq(){
     lock.lock();
     try {
     while (队列已空){
      // 等待队列不空
      notEmpty.await();
     }
     // 省略出队 *** 作...
     // 出队后,通知可入队
     notFull.signal();
    }finally {
      lock.unlock();
    }
  }
}

这里你需要注意,Lock 和 Condition 实现的管程,线程等待和通知需要调用
await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。

Condition在Dubbo中的应用

RPC 调用,在 TCP 协议层面,发送完 RPC 请求后,线程是不会等待 RPC 的响应结果的。平时工作中的 RPC 调用大多数都是同步的啊?这是怎么回事呢?其实很简单,一定是有人帮你做了异步转同步的事情。例如目前知名的 RPC 框架 Dubbo就给我们做了异步转同步的事情。

如果此时你将调用线程 dump 出来的话,你会发现调用线程阻塞了,**线程状态是 TIMED_WAITING。本来发送请求是异步的,但是调用线程却阻塞了,说明Dubbo 帮我们做了异步转同步的事情。**通过调用栈,你能看到线程是阻塞在DefaultFuture.get() 方法上,所以可以推断:Dubbo 异步转同步的功能应该是通过DefaultFuture 这个类实现的。

public class DubboInvoker{
   Result doInvoke(Invocation inv){
   // 下面这行就是源码中 108 行
   // 为了便于展示,做了修改
   return currentClient.request(inv, timeout).get();
   }
}

当 RPC 返回结果之前,阻塞调用线程,让调用线程等待;当 RPC 返回结果后,唤醒调用线程,让调用线程重新执行。不知道你有没有似曾相识的感觉,这不就是经典的等待 - 通知机制吗?主要实现代码如下:

// 创建锁与条件变量
private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition();
// 调用方通过该方法等待结果
Object get(int timeout){
  long start = System.nanoTime();
  lock.lock();
  try {
    while (!isDone()) {
      done.await(timeout);
      long cur=System.nanoTime();
      if (isDone() || cur-start > timeout){
        break;
      }
    }
  } finally {
    lock.unlock();
 }
 if (!isDone()) {
    throw new TimeoutException();
 }
  return returnFromResponse();
}
// RPC 结果是否已经返回
boolean isDone() {
  return response != null;
}
// RPC 结果返回时调用该方法
private void doReceived(Response res) {
  lock.lock();
  try {
    response = res;
    if (done != null) {
      done.signal();
    }
  } finally {
    lock.unlock();
  }
}

调用线程通过调用 get() 方法等待 RPC 返回结果,这个方法里面,你看到的都是熟悉的“面孔”:调用 lock() 获取锁,在 finally 里面调用 unlock() 释放锁;获取锁后,通过经典的在循环中调用 await() 方法来实现等待。当 RPC 结果返回时,会调用 doReceived() 方法,这个方法里面,调用 lock() 获取锁,在finally 里面调用 unlock() 释放锁,获取锁后通过调用 signal() 来通知调用线程,结果已经返回,不用继续等待了。

总结:
觉得有用的客官可以点赞、关注下!感谢支持谢谢!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5138230.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-17
下一篇 2022-11-17

发表评论

登录后才能评论

评论列表(0条)

保存