Unsafe 魔法类应用体现 (LockSupport)

政采云技术团队.png

作者-斜照

前言

​ 随着科技的进步,计算机硬件和算力也迎来跨越式的发展,从单核 CPU 串行执行计算任务到如今多核 CPU 并行执行任务。

为了充分利用多核资源,并发编程技术应时代出现。

​ 并发编程在提高程序执行效率的同时,也带来了技术上的挑战,比如多线程之间如何通信,常见的解决方案是通过管道或者共享内存。Java 作为比较流行的编程语言,自然也是要顺应并发的潮流,Java 采用共享内存方式保障多线程之间的通信。

​ 使用共享内存通信,也带来了并发的相关问题:可见性、原子性、有序性。

​ 其中原子性则是由线程切换导致的并发问题,线程如何调度也是一门精细活,本文则从线程阻塞和唤醒的调度角度,来探讨和揭开一部分 Unsafe 魔法类的面纱。

阻塞和唤醒

​ 艺术来源于生活,一些技术上的解决方案也可以体现在日常生活中。

​ 如社区医院的就诊过程,患者取号->医生叫号->诊断->检查室检查->拿到结果复诊->诊断结束-叫下一位,把医生和患者看做线程和任务,则等待患者和诊断则是阻塞和唤醒的场景。

​ 基于该场景,做一下代码实现:

public class test07 {
    public static void main(String[] args) {
        List<Patient> list = Arrays.asList(
                new Patient(Check.BRAIN.getType()),
                new Patient(Check.EYES.getType()),
                new Patient(Check.OTHER.getType())
        );
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (Patient patient : list) {
            executorService.submit(new Doctor(patient));
            executorService.submit(new SpecialExamination(patient));
        }
        executorService.shutdown();
        System.out.println("ok");
    }
}

class Doctor implements Runnable {

    private Patient patient;

    public Doctor(Patient patient) {
        this.patient = patient;
    }

    public void run() {
        /**
         * 1.检查是否挂号
         * 2.是否需要检查原因
         * 3.处理其他患者
         * */
        synchronized (this.patient) {
            while (null == patient.getReason()) {
                System.out.println("医生" + Thread.currentThread().getName() + "单号" + patient.getNumber() + "需要进行检查");
                try {
                    patient.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("医生" + Thread.currentThread().getName() + "开始治疗单号" + patient.getNumber() + "类型" + patient.getType() + "原因" + patient.getReason());
            return;
        }
    }
}

class SpecialExamination implements Runnable {

    private Patient patient;

    public SpecialExamination(Patient patient) {
        this.patient = patient;
    }

    public void run() {
        synchronized (patient) {
            if (patient.getType() == Check.BRAIN.getType()) {
                patient.setReason("脑子坏了");
            } else if (patient.getType() == Check.EYES.getType()) {
                patient.setReason("头发没了");
            } else if (patient.getType() == Check.OTHER.getType()) {
                patient.setReason("其他原因");
            }
            System.out.println("通知单号" + patient.getNumber() + "可以开始会诊");
            patient.notifyAll();
        }
    }
}

enum Check {
    BRAIN(1),
    EYES(2),
    OTHER(3);

    public int getType() {
        return type;
    }

    private int type;

    Check(int type) {
        this.type = type;
    }
}

class Patient {
    private String number;
    private int type;
    private String reason;

    public Patient(int type) {
        this.number = UUID.randomUUID().toString();
        this.type = type;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }
}  

运行结果:
医生pool-1-thread-1单号284a4fe2-4692-47b7-8466-44b36a1d6265需要进行检查
通知单号284a4fe2-4692-47b7-8466-44b36a1d6265已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号284a4fe2-4692-47b7-8466-44b36a1d6265类型1原因脑子坏了
医生pool-1-thread-3单号76afe44b-2bb2-40e9-9201-ef4d24a95619需要进行检查
通知单号76afe44b-2bb2-40e9-9201-ef4d24a95619已经拿到检查结果可以开始会诊
医生pool-1-thread-3开始治疗单号76afe44b-2bb2-40e9-9201-ef4d24a95619类型2原因眼镜坏了
医生pool-1-thread-5单号c98b41de-6ab8-41a7-8c11-9838eeacd73e需要进行检查
通知单号c98b41de-6ab8-41a7-8c11-9838eeacd73e已经拿到检查结果可以开始会诊
分诊结束
医生pool-1-thread-5开始治疗单号c98b41de-6ab8-41a7-8c11-9838eeacd73e类型3原因其他原因

​ 由上代码输出可以看到每个 "医生" 都让 "患者" 去做检查,检查完毕后再由当前 "医生" 进行会诊。

​ "医生" 线程接待任务,然后任务流转到检查线程,"医生" 线程阻塞等待检查线程执行完毕后被唤醒执行剩下的事情。

​ 上面的实现借助 wait & notify 来实现阻塞和唤醒,如果唤醒线程先执行,阻塞线程后执行,那么被挂起的线程将永远无法被唤醒,无法达到预期结果,所以需要引入锁来保障执行顺序。

​ 如果不借助 wait & notify 和锁能实现阻塞和唤醒吗?当然可以,下面看一下使用 LockSupport 如何达成效果吧。

public class test07 {
    public static void main(String[] args) {
        List<Patient> list = Arrays.asList(
                new Patient(Check.BRAIN.getType()),
                new Patient(Check.EYES.getType()),
                new Patient(Check.OTHER.getType())
        );
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (Patient patient : list) {
            executorService.submit(new Doctor(patient));
            executorService.submit(new SpecialExamination(patient));
        }
        executorService.shutdown();
        System.out.println("分诊结束");
    }
}

class Doctor implements Runnable {

    private Patient patient;

    public Doctor(Patient patient) {
        this.patient = patient;
    }

    @Override
    public void run() {
        /**
         * 1.检查是否挂号
         * 2.是否需要检查原因
         * 3.处理其他患者
         * */
        while (null == patient.getReason()) {
            System.out.println("医生" + Thread.currentThread().getName() + "单号" + patient.getNumber() + "需要进行检查");
            patient.setCurThread(Thread.currentThread());
            LockSupport.park(patient);
        }
        System.out.println("医生" + Thread.currentThread().getName() + "开始治疗单号" + patient.getNumber() + "类型" + patient.getType() + "原因" + patient.getReason());
        return;
    }
}

class SpecialExamination implements Runnable {

    private Patient patient;

    public SpecialExamination(Patient patient) {
        this.patient = patient;
    }

    @Override
    public void run() {
            if (patient.getType() == Check.BRAIN.getType()) {
                patient.setReason("脑子坏了");
            } else if (patient.getType() == Check.EYES.getType()) {
                patient.setReason("眼镜坏了");
            } else if (patient.getType() == Check.OTHER.getType()) {
                patient.setReason("其他原因");
            }
            System.out.println("通知单号" + patient.getNumber() + "已经拿到检查结果可以开始会诊");
            LockSupport.unpark(patient.getCurThread());
    }
}

enum Check {
    BRAIN(1),
    EYES(2),
    OTHER(3);

    public int getType() {
        return type;
    }

    private int type;

    Check(int type) {
        this.type = type;
    }
}

class Patient {
    private String number;
    private int type;
    private String reason;

    public Thread getCurThread() {
        return curThread;
    }

    public void setCurThread(Thread curThread) {
        this.curThread = curThread;
    }

    private Thread curThread;

    public Patient(int type) {
        this.number = UUID.randomUUID().toString();
        this.type = type;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }
}

运行结果:
医生pool-1-thread-1单号f1cd75b6-3b08-4d91-86c8-b2840d586b42需要进行检查
通知单号f1cd75b6-3b08-4d91-86c8-b2840d586b42已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号f1cd75b6-3b08-4d91-86c8-b2840d586b42类型1原因脑子坏了
医生pool-1-thread-3单号0a1dad26-9569-494f-8c87-6185aa453d76需要进行检查
通知单号0a1dad26-9569-494f-8c87-6185aa453d76已经拿到检查结果可以开始会诊
医生pool-1-thread-1单号56202b98-789f-4bd3-ba5d-c0809559b3e2需要进行检查
医生pool-1-thread-3开始治疗单号0a1dad26-9569-494f-8c87-6185aa453d76类型2原因眼镜坏了
分诊结束
通知单号56202b98-789f-4bd3-ba5d-c0809559b3e2已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号56202b98-789f-4bd3-ba5d-c0809559b3e2类型3原因其他原因  

​ 上代码块做了一些改造将 "医生" 线程对象与 "患者" 任务进行了绑定,使用 LockSupport 进行阻塞和唤醒,由结果可得,两个 "医生" 线程都有预期的表现。

​ LockSupport 在不使用锁的情况下完成了线程的预期调度。它是如何做到的呢?下文尝试从源码角度探讨背后的隐秘。

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    private static void setBlocker(Thread t, Object arg) {
        
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }
    private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }

}

​ 上代码块是精简过后的相关源码,该类相关功能 API 如下:

LockSuppoort

​ 粗略阅读了这四个方法,一头雾水,LockSupport 内部定义了大量的变量,多处使用了 Unsafe 类,想要读懂它, 要先打破它的铠甲,通读全类发现多处依赖了 Unsafe 这个静态类,这个类是什么作用呢,来探一探究竟。

Unsafe

什么是 Unsafe 类?

​ Java 是一种高级抽象语言,直接针对于系统底层操作调用方式较少,有些情况需要直接访问系统内存资源并自主管理资源,为了较高的安全性舍弃了指针概念,所以当某些情况需要提升Java运行效率增强计算机底层资源访问能力,所以产生了 Unsafe 类,用来替代部分指针角色。

​ 由于 UNSAFE 可以直接操作计算机底层资源引起资源管理泄露问题,所以使用 Unsafe 时要严谨<对于 Unsafe 类的调用方式,本文一笔带过,可凭借兴趣自行查阅>。

Unsafe 类可以做什么呢?

​ Unsafe 类中提供的 API 大致作用分类:

内存操作、CAS、对象操作、系统相关、线程调度、数组操作、Class相关、内存屏障。 Unsafe

​ 初步了解 Unsafe 这个魔法类后,发现它竟然提供了这么丰富的内容,那么再回头读懂 LockSupport 源码对一些变量的位移获取和设置就不会一头雾水了,可以做出如下解读:

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        // 获取当前线程对象parkBlocker字段的位移偏移量
        parkBlockerOffset = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("parkBlocker"));
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception ex) { throw new Error(ex); }
}

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
     // 通过unsafe设置互斥变量
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
      // 清空互斥变量
    setBlocker(t, null);
}

 private static void setBlocker(Thread t, Object arg) {
        // 当前线程对象的parkBlocker属性设置arg
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

​ park 和 unpark 中调用了 Unsafe 的 park 和 unpark,这两个方法被 native 修饰,说明调用了C实现的原生函数,不过核心在这两个方法内,他们内部做了什么呢,只能继续挖掘源码,去官网扒拉出原生函数源码。如下:

parker源码

park源码逻辑:

void Parker::park(bool isAbsolute, jlong time) {
   // 原子交换操作将计数器的值改为0,同时检查计数器的原值是否大于 0,即_counter>0,如果大于0<执行unpark将_counter改为1>则表示unpark先于park执行,本次park直接返回,否则往下执行
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  JavaThread *jt = (JavaThread *)thread;
  // 判断当前线程是否标记了中断,如果有中断则直接返回
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  timespec absTime;
  if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
    return;
  }
  if (time > 0) {
    unpackTime(&absTime, isAbsolute, time);
  }


  ThreadBlockInVM tbivm(jt);

  // // 再次判断当前线程是否标记了中断,如果有中断则直接返回,否则通过pthread_mutex_trylock增加互斥锁<mutex>
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    return;
  }

  int status ;
  // 以上都不命中时,再次判断计数器是否执行过unpark
  if (_counter > 0)  { // no wait needed
   // 如果执行过unpark,则将计数器重置
    _counter = 0;
    // 计数器重置后完成互斥锁释放
    status = pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
    // Paranoia to ensure our locked and lock-free paths interact
    // correctly with each other and Java-level accesses.
    OrderAccess::fence();
    return;
  }
 
  sigset_t oldsigs;
  sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals();
  pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs);

  OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
  jt->set_suspend_equivalent();

  assert(_cur_index == -1, "invariant");
  // 当无等待时长时,且未执行过unpark,调用pthread_cond_wait函数,查阅得该函数它会使当前线程加入操作系统的条件等待队列,同时释放 mutex 锁并使当前线程挂起
  // 等待其他线程执行pthread_cond_signal唤醒当前
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
    if (status != 0 && WorkAroundNPTLTimedWaitHang) {
      pthread_cond_destroy (&_cond[_cur_index]) ;
      pthread_cond_init    (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
    }
  }
  _cur_index = -1;
  assert_status(status == 0 || status == EINTR ||
                status == ETIME || status == ETIMEDOUT,
                status, "cond_timedwait");

#ifdef ASSERT
  pthread_sigmask(SIG_SETMASK, &oldsigs, NULL);
#endif

  _counter = 0 ;
  status = pthread_mutex_unlock(_mutex) ;
  assert_status(status == 0, status, "invariant") ;
  OrderAccess::fence();

  if (jt->handle_special_suspend_equivalent_condition()) {
    jt->java_suspend_self();
  }
} 

unpark源码逻辑:

void Parker::unpark() {
  int s, status ;
  // 调用pthread_mutex_lock函数,给该线程加互斥锁,并通过下赋值操作将计数器赋值为1
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ;
  s = _counter;
  _counter = 1;
  if (s < 1) {
    // 计数器小于1则当前线程被执行过park
    if (_cur_index != -1) {
      // 判断 Parker 对象所关联的线程是否被 park,如果是,则通过 pthread_mutex_signal 函数唤醒该线程,最后释放锁。
      // pthread_cond_signal和pthread_cond_wait配套使用,当前线程调用unpark的pthread_cond_signal后,则唤醒操作系统中某个执行pthread_cond_wait的等待线程并设置_cur_index=-1
      if (WorkAroundNPTLTimedWaitHang) {
        status = pthread_cond_signal (&_cond[_cur_index]);
        assert (status == 0, "invariant");
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
      } else {
        // must capture correct index before unlocking
        int index = _cur_index;
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
        status = pthread_cond_signal (&_cond[index]);
        assert (status == 0, "invariant");
      }
    } else {
      pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant") ;
    }
  } else {
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

梳理一下park和unpark执行逻辑:

park and unpark

​ Unsafe 的 park 和 unpark 主要依靠系统的 pthread_cond_signal 和 pthread_cond_wait 对线程进行挂起和恢复,来达成调度中的阻塞和唤醒效果,值得一提的是,wait 和 notify 底层也是依附这两个函数进行线程挂起和恢复。

​ 经过以上内容对源码的分析,已然对 LockSupport 有一个简单的认识,能够不通过 Java 锁来协调线程挂起和唤醒,还有神奇的 Unsafe 魔法类,此刻估计已经按奈不住跃跃欲试的心,那就快快把脑袋里的想法变成代码吧。

推荐阅读

数据中台建设实践(二)- 数据治理之数据质量

RocketMQ 延时方案分析与总结

政采云Flutter低成本屏幕适配方案探索

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png