关于java:60行自己动手写LockSupport是什么体验

34次阅读

共计 6916 个字符,预计需要花费 18 分钟才能阅读完成。

60 行本人入手写 LockSupport 是什么体验?

前言

在 JDK 当中给咱们提供的各种并发工具当中,比方 ReentrantLock 等等工具的外部实现,常常会应用到一个工具,这个工具就是 LockSupportLockSupport 给咱们提供了一个十分弱小的性能,它是线程阻塞最根本的元语,他能够将一个线程阻塞也能够将一个线程唤醒,因而常常在并发的场景下进行应用。

LockSupport 实现原理

在理解 LockSupport 实现原理之前咱们先用一个案例来理解一下 LockSupport 的性能!

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {System.out.println("park 之前");
      LockSupport.park(); // park 函数能够将调用这个办法的线程挂起
      System.out.println("park 之后");
    });
    thread.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("主线程劳动了 5s");
    System.out.println("主线程 unpark thread");
    LockSupport.unpark(thread); // 主线程将线程 thread 唤醒 唤醒之后线程 thread 才能够继续执行
  }
}

下面的代码的输入如下:

park 之前
主线程劳动了 5s
主线程 unpark thread
park 之后

乍一看下面的 LockSupport 的 park 和 unpark 实现的性能和 await 和 signal 实现的性能如同是一样的,然而其实不然,咱们来看上面的代码:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo02 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {
      try {TimeUnit.SECONDS.sleep(5);
      } catch (InterruptedException e) {e.printStackTrace();
      }
      System.out.println("park 之前");
      LockSupport.park(); // 线程 thread 后进行 park 操作 
      System.out.println("park 之后");
    });
    thread.start();
    System.out.println("主线程 unpark thread");
    LockSupport.unpark(thread); // 先进行 unpark 操作

  }
}

下面代码输入后果如下:

主线程 unpark thread
park 之前
park 之后

在下面的代码当中主线程会先进行 unpark 操作,而后线程 thread 才进行 park 操作,这种状况下程序也能够失常执行。然而如果是 signal 的调用在 await 调用之前的话,程序则不会执行实现,比方上面的代码:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Demo03 {private static final ReentrantLock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();

  public static void thread() throws InterruptedException {lock.lock();

    try {TimeUnit.SECONDS.sleep(5);
      condition.await();
      System.out.println("期待实现");
    }finally {lock.unlock();
    }
  }

  public static void mainThread() {lock.lock();
    try {System.out.println("发送信号");
      condition.signal();}finally {lock.unlock();
      System.out.println("主线程解锁实现");
    }
  }

  public static void main(String[] args) {Thread thread = new Thread(() -> {
      try {thread();
      } catch (InterruptedException e) {e.printStackTrace();
      }
    });
    thread.start();

    mainThread();}
}

下面的代码输入如下:

发送信号
主线程解锁实现

在下面的代码当中“期待实现“始终是不会被打印进去的,这是因为 signal 函数的调用在 await 之前,signal 函数只会对在它之前执行的 await 函数有成果,对在其前面调用的 await 是不会产生影响的。

那是什么起因导致的这个成果呢?

其实 JVM 在实现 LockSupport 的时候,外部会给每一个线程保护一个计数器变量_counter,这个变量是示意的含意是“许可证的数量”,只有当有许可证的时候线程才能够执行,同时许可证最大的数量只能为 1。当调用一次 park 的时候许可证的数量会减一。当调用一次 unpark 的时候计数器就会加一,然而计数器的值不能超过 1

当一个线程调用 park 之后,他就须要期待一个许可证,只有拿到许可证之后这个线程才可能继续执行,或者在 park 之前曾经取得一个了一个许可证,那么它就不须要阻塞,间接能够执行。

本人入手实现本人的 LockSupport

实现原理

在前文当中咱们曾经介绍了 locksupport 的原理,它次要的外部实现就是通过许可证实现的:

  • 每一个线程可能获取的许可证的最大数目就是 1。
  • 当调用 unpark 办法时,线程能够获取一个许可证,许可证数量的下限是 1,如果曾经有一个许可证了,那么许可证就不能累加。
  • 当调用 park 办法的时候,如果调用 park 办法的线程没有许可证的话,则须要将这个线程挂起,直到有其余线程调用 unpark 办法,给这个线程发放一个许可证,线程才可能继续执行。然而如果线程曾经有了一个许可证,那么线程将不会阻塞能够间接执行。

本人实现 LockSupport 协定规定

在咱们本人实现的 Parker 当中咱们也能够给每个线程一个计数器,记录线程的许可证的数目,当许可证的数目大于等于 0 的时候,线程能够执行,反之线程须要被阻塞,协定具体规定如下:

  • 初始线程的许可证的数目为 0。
  • 如果咱们在调用 park 的时候,计数器的值等于 1,计数器的值变为 0,则线程能够继续执行。
  • 如果咱们在调用 park 的时候,计数器的值等于 0,则线程不能够继续执行,须要将线程挂起,且将计数器的值设置为 -1。
  • 如果咱们在调用 unpark 的时候,被 unpark 的线程的计数器的值等于 0,则须要将计数器的值变为 1。
  • 如果咱们在调用 unpark 的时候,被 unpark 的线程的计数器的值等于 1,则不须要扭转计数器的值,因为计数器的最大值就是 1。
  • 咱们在调用 unpark 的时候,如果计数器的值等于 -1,阐明线程曾经被挂起了,则须要将线程唤醒,同时须要将计数器的值设置为 0。

工具

因为波及线程的阻塞和唤醒,咱们能够应用可重入锁 ReentrantLock 和条件变量Condition,因而须要相熟这两个工具的应用。

  • ReentrantLock 次要用于加锁和开锁,用于爱护临界区。
  • Condition.awat 办法用于将线程阻塞。
  • Condition.signal 办法用于将线程唤醒。
  • 因为咱们在 unpark 办法当中须要传入具体的线程,将这个线程发放许可证,同时唤醒这个线程,因为是须要针对特定的线程进行唤醒,而 condition 唤醒的线程是不确定的,因而咱们须要为每一个线程保护一个 计数器 条件变量,这样每个条件变量只与一个线程相干,唤醒的必定就是一个特定的线程。咱们能够应用 HashMap 进行实现,键为线程,值为计数器或者条件变量。

具体实现

  • 因而综合下面的剖析咱们的类变量如下:
private final ReentrantLock lock; // 用于爱护临界去
private final HashMap<Thread, Integer> permits; // 许可证的数量
private final HashMap<Thread, Condition> conditions; // 用于唤醒和阻塞线程的条件变量
  • 构造函数次要对变量进行赋值:
public Parker() {lock = new ReentrantLock();
  permits = new HashMap<>();
  conditions = new HashMap<>();}
  • park 办法
public void park() {Thread t = Thread.currentThread(); // 首先失去以后正在执行的线程
  if (conditions.get(t) == null) { // 如果还没有线程对应的 condition 的话就进行创立
    conditions.put(t, lock.newCondition());
  }
  lock.lock();
  try {
    // 如果许可证变量还没有创立 或者许可证等于 0 阐明没有许可证了 线程须要被挂起
    if (permits.get(t) == null || permits.get(t) == 0) {permits.put(t, -1); // 同时许可证的数目应该设置为 -1
      conditions.get(t).await();}else if (permits.get(t) > 0) {permits.put(t, 0); // 如果许可证的数目大于 0 也就是为 1 阐明线程曾经有了许可证因而能够间接被放行 然而须要耗费一个许可证
    }
  } catch (InterruptedException e) {e.printStackTrace();
  } finally {lock.unlock();
  }
}

  • unpark 办法
public void unpark(Thread thread) {
  Thread t = thread; // 给线程 thread 发放一个许可证
  lock.lock();
  try {if (permits.get(t) == null) // 如果还没有创立许可证变量 阐明线程以后的许可证数量等于初始数量也就是 0 因而办法许可证之后 许可证的数量为 1
      permits.put(t, 1);
    else if (permits.get(t) == -1) { // 如果许可证数量为 -1,则阐明必定线程 thread 调用了 park 办法,而且线程 thread 曾经被挂起了 因而在 unpark 函数当中不急须要将许可证数量这是为 0 同时还须要将线程唤醒
      permits.put(t, 0);
      conditions.get(t).signal();}else if (permits.get(t) == 0) { // 如果许可证数量为 0 阐明线程正在执行 因而许可证数量加一
      permits.put(t, 1);
    } // 除此之外就是许可证为 1 的状况了 在这种状况下是不须要进行操作的 因为许可证最大的数量就是 1
  }finally {lock.unlock();
  }
}

残缺代码

import java.util.HashMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Parker {

  private final ReentrantLock lock;
  private final HashMap<Thread, Integer> permits;
  private final HashMap<Thread, Condition> conditions;

  public Parker() {lock = new ReentrantLock();
    permits = new HashMap<>();
    conditions = new HashMap<>();}

  public void park() {Thread t = Thread.currentThread();
    if (conditions.get(t) == null) {conditions.put(t, lock.newCondition());
    }
    lock.lock();
    try {if (permits.get(t) == null || permits.get(t) == 0) {permits.put(t, -1);
        conditions.get(t).await();}else if (permits.get(t) > 0) {permits.put(t, 0);
      }
    } catch (InterruptedException e) {e.printStackTrace();
    } finally {lock.unlock();
    }
  }

  public void unpark(Thread thread) {
    Thread t = thread;
    lock.lock();
    try {if (permits.get(t) == null)
        permits.put(t, 1);
      else if (permits.get(t) == -1) {permits.put(t, 0);
        conditions.get(t).signal();}else if (permits.get(t) == 0) {permits.put(t, 1);
      }
    }finally {lock.unlock();
    }
  }
}

JVM 实现一瞥

其实在 JVM 底层对于 park 和 unpark 的实现也是基于锁和条件变量的,只不过是用更加底层的操作系统和 libc(linux 操作系统)提供的 API 进行实现的。尽管 API 不一样,然而原理是相仿的,思维也类似。

比方上面的就是 JVM 实现的 unpark 办法:

void Parker::unpark() {
  int s, status;
  // 进行加锁操作 相当于 可重入锁的 lock.lock()
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant");
  s = _counter;
  _counter = 1;
  if (s < 1) {
    // 如果许可证小于 1 进行上面的操作
    if (WorkAroundNPTLTimedWaitHang) {// 这行代码相当于 condition.signal() 唤醒线程
      status = pthread_cond_signal (_cond);
      assert (status == 0, "invariant");
      // 解锁操作 相当于可重入锁的 lock.unlock()
      status = pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant");
    } else {status = pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant");
      status = pthread_cond_signal (_cond);
      assert (status == 0, "invariant");
    }
  } else {
    // 如果有许可证 也就是 s == 1 那么不许要将线程挂起
    // 解锁操作 相当于可重入锁的 lock.unlock()
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant");
  }
}

JVM 实现的 park 办法,如果没有许可证也是会将线程挂起的:

总结

在本篇文章当中次要介绍啦 lock support 的用法以及它的大抵原理,以及介绍啦咱们本人该如何实现相似 lock support 的性能,并且定义了咱们本人实现 lock support 的大抵协定,整个过程还是比拟清晰的,咱们只是实现了 lock support 当中两个外围办法,其余的办法其实也相似,原理差不多,在这里咱就实现一个乞丐版的 lock support 的吧!!!

  • 应用锁和条件变量进行线程的阻塞和唤醒。
  • 应用 Thread.currentThread() 办法失去以后正在执行的线程。
  • 应用 HashMap 去存储线程和许可证以及条件变量的关系。

以上就是本篇文章的所有内容了,我是LeHung,咱们下期再见!!!更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0