暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

95后百万年薪架构师分享:两道BT的多线程经典面试题 (一) | 干货分享

马士兵 2021-07-06
688

今天我们来分享两道两道BT的多线程经典面试题,作者是黄俊老师。

黄俊老师,95后、23岁拿到阿里60w年薪,之后在美团拿到百万年薪,现在一心只想钻研技术。

由于文章篇幅较长,我们会把两个问题分开来讲,避免有小伙伴搞不懂,接下来我们先来看第一个问题:

线程唤醒问题

样例代码:

    public class Test {
    /**
    * 有三个线程 A,B,C
    * A为什么总是在C前面抢到锁???
    */
    private final static Object LOCK = new Object();


    public void startThreadA() {
    new Thread(() -> {
    synchronized (LOCK) {
    System.out.println(Thread.currentThread().getName() + ": get lock");
    //启动线程b
    startThreadB();
    System.out.println(Thread.currentThread().getName() + ": start wait");
    try {
    //线程a wait
    LOCK.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ": get lock after wait");
    System.out.println(Thread.currentThread().getName() + ": release lock");
    }
    }, "thread-A").start();
    }


    private void startThreadB() {
    new Thread(() -> {
    synchronized (LOCK) {
    System.out.println(Thread.currentThread().getName() + ": get lock");
    //启动线程c
    startThreadC();
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ": start notify");
    //线程b唤醒其他线程
    LOCK.notify();
    System.out.println(Thread.currentThread().getName() + ": release lock");
    }
    }, "thread-B").start();
    }


    private void startThreadC() {
    new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + ": thread c start");
    synchronized (LOCK) {
    System.out.println(Thread.currentThread().getName() + ": get lock");
    System.out.println(Thread.currentThread().getName() + ": release lock");
    }
    }, "thread-C").start();
    }


    public static void main(String[] args) {
    new Test().startThreadA();
    }
    }

    输出结果:

      thread-A: get lock
      thread-A: start wait
      thread-B: get lock
      thread-C: thread c start
      thread-B: start notify
      thread-B: release lock
      thread-A: get lock after wait
      thread-A: release lock
      thread-C: get lock
      thread-C: release lock
      问题:
      为什么每次运行,线程A总是优先于线程C获取锁?

      分析:

      在Hotspot源码中,我们知道synchronized关键字是通过monitor_enter和monitor_exit字节来实现的,最终用于阻塞线程的对象为ObjectMonitor对象,该对象包含三个关键字段:WaitSet、cxq、EntryList。
      WaitSet用于保存使用wait方法释放获得的synchronized锁对象的线程,也即我们调用wait函数,那么当前线程将会释放锁,并将自身放入等待集中。
      而cxq队列用于存放竞争ObjectMonitor锁对象失败的线程,而_EntryList用于也用于存放竞争锁失败的线程。
      那么它们之间有何区别呢?这是由于我们需要频繁的释放和获取锁,当我们获取锁失败那么将需要把线程放入竞争列表中,当唤醒时需要从竞争列表中获取线程唤醒获取锁。
      而如果我们只用一个列表来完成这件事,那么将会导致锁争用导致CPU资源浪费且影响性能,这时我们独立出两个列表,其中cxq列表用于竞争放入线程,而entrylist用于单线程唤醒操作。具体策略是这样的:
      1. 线程竞争锁失败后CAS放入cxq列表中
      2. 线程释放锁后将根据策略来唤醒cxq或者entrylist中的线程(我们这里只讨论默认策略)
      3. 默认策略下优先唤醒entrylist列表中的线程,因为唤醒线程对象的操作是单线程的,也即只有获取锁并且释放锁的线程可以操作,所以操作entrylist是线程安全的
      4. 如果entrylist列表为空,那么将会CAS将cxq中的等待线程一次性获取到entrylist中并开始逐个唤醒
      在hotspot中我们称这种算法为电梯算法,也即将需要唤醒的线程一次性从竞争队列中放入entrylist唤醒队列。
      那么这时我们就可以分析以上代码为何总是唤醒线程A了。
      我们先看线程执行顺序,首先启动线程A,随后线程A启动线程B,B线程需要获取对象锁从而创建线程C。
      我们看到当线程A调用wait方法将自己放入等待集中后,将会唤醒线程B,随后线程B创建并启动了线程C,然后等待C开始执行,由于此时对象锁由线程B持有,所以线程C需要放入cxq竞争队列。
      随后B从睡眠中醒来,执行notify方法,该方法总是唤醒了线程A而不是C,也即优先处理等待集中的线程而不是cxq竞争队列的线程。
      那么我们通过notify方法来看看实现原理。
      Notify便是Wait操作的反向操作,所以这里很简单,无非就是将线程从等待集中移出并且唤醒,源码如下:
        JVM_ENTRY(void, JVM_MonitorNotify(JNIEnv* env, jobject handle))
        Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
        // 直接调用ObjectSynchronizer::notify
        ObjectSynchronizer::notify(obj, CHECK);
        JVM_END
        这里直接跟进ObjectSynchronizer::notify,源码如下:
          void ObjectSynchronizer::notify(Handle obj, TRAPS) {
          if (UseBiasedLocking) {
          // 如果使用偏向锁,那么取消偏向锁
          BiasedLocking::revoke_and_rebias(obj, false, THREAD);
          }
          markOop mark = obj->mark();
          if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
          // 如果是轻量级锁,那么直接返回,因为wait操作需要通过对象监视器来做
          return;
          }
          ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
          }
          可以看到最终调用了ObjectSynchronizer的notify方法来唤醒,源码如下:
            void ObjectMonitor::notify(TRAPS) {
            CHECK_OWNER();
            if (_WaitSet == NULL) {
            // 如果等待集为空,直接返回
            return ;
            }
            int Policy = Knob_MoveNotifyee ; // 移动策略,这里默认是2
            Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ; // 首先对等待集上自旋锁
            // 调用DequeueWaiter将一个等待线程从等待集中拿出来
            ObjectWaiter * iterator = DequeueWaiter() ;
            if (iterator != NULL) {
            if (Policy != 4) { // 如果策略不等于4那么将线程的状态修改为TS_ENTER
            iterator->TState = ObjectWaiter::TS_ENTER ;
            }
            iterator->_notified = 1 ; // 唤醒计数器
            Thread * Self = THREAD;
            iterator->_notifier_tid = Self->osthread()->thread_id();
            ObjectWaiter * List = _EntryList ;
            if (Policy == 0) { // 如果策略为0,那么头插入到entrylist中
            if (List == NULL) { // 如果entrylist为空,那么将当前监视器直接作为_EntryList 头结点
            iterator->_next = iterator->_prev = NULL ;
            _EntryList = iterator ;
            } else { // 否则头插
            List->_prev = iterator ;
            iterator->_next = List ;
            iterator->_prev = NULL ;
            _EntryList = iterator ;
            }
            } else if (Policy == 1) { // 如果策略为1,那么插入entrylist的尾部
            if (List == NULL) {
            iterator->_next = iterator->_prev = NULL ;
            _EntryList = iterator ;
            } else {
            ObjectWaiter * Tail ;
            for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ;
            Tail->_next = iterator ;
            iterator->_prev = Tail ;
            iterator->_next = NULL ;
            }
            } else if (Policy == 2) {
            // 如果策略为2,那么如果entrylist为空,那么插入entrylist,否则插入cxq队列
            if (List == NULL) {
            iterator->_next = iterator->_prev = NULL ;
            _EntryList = iterator ;
            } else {
            iterator->TState = ObjectWaiter::TS_CXQ ;
            for (;;) {
            ObjectWaiter * Front = _cxq ;
            iterator->_next = Front ;
            if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
            break ;
            }
            }
            }
            } else
            if (Policy == 3) { // 如果策略为3,那么直接插入cxq
            iterator->TState = ObjectWaiter::TS_CXQ ;
            for (;;) {
            ObjectWaiter * Tail ;
            Tail = _cxq ;
            if (Tail == NULL) {
            iterator->_next = NULL ;
            if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) {
            break ;
            }
            } else {
            while (Tail->_next != NULL) Tail = Tail->_next ;
            Tail->_next = iterator ;
            iterator->_prev = Tail ;
            iterator->_next = NULL ;
            break ;
            }
            }
            } else {
            // 否则直接唤醒线程,让线程自己去调用enterI进入监视器
            ParkEvent * ev = iterator->_event ;
            iterator->TState = ObjectWaiter::TS_RUN ;
            OrderAccess::fence() ;
            ev->unpark() ;
            }
            }
            Thread::SpinRelease (&_WaitSetLock) ; // 释放等待集自旋锁
            }
            这里有一个方法DequeueWaiter() 将线程从等待集中取出来,这里的notify读者都知唤醒一个,很多人都说随机唤醒一个,那么我们这里来看看唤醒算法是什么,源码如下:
              inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
              ObjectWaiter* waiter = _WaitSet; // 很简单对吧,直接从头部拿
              if (waiter) { // 如果waiter不为空,那么从等待集中断链
              DequeueSpecificWaiter(waiter);
              }
              return waiter;
              }
              inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) {
              ObjectWaiter* next = node->_next;
              if (next == node) { // 如果只有一个节点,那么直接将等待集清空即可
              _WaitSet = NULL;
              } else { // 否则双向链表的断链基础操作
              ObjectWaiter* prev = node->_prev;
              next->_prev = prev;
              prev->_next = next;
              if (_WaitSet == node) {
              _WaitSet = next;
              }
              }
              // 断开连接后,也需要把断下来的节点,next和prev指针清空
              node->_next = NULL;
              node->_prev = NULL;
              }
              那么读者应该可以明显的看到,底层对于唤醒操作是从等待集的头部选择线程唤醒。

              总结:

              通过源码我们看到,为何总是唤醒线程A,这是用于当线程C竞争不到锁时,被放入了cxq队列,而此时entrylist为null,线程A在等待集waitset中,当我们调用notify方法时,由于移动策略默认是2,这时会从等待集的头部将线程A取下,放入到entrylist中,当notify执行完毕后,在执行后面的monitor_exit字节码时将会优先从entrylist中唤醒线程,这就导致了A线程总是被优先执行。
              由于篇幅有限,今天我们先发一道,明天更新第二篇 线程执行完isAlive方法返回true问题》。
              明天记得来看哦~
              关于作者:

              👇推荐关注👇

              有趣的行业资讯

              干货技术分享

              程序员的日常生活

              ......

              干就完了
              文章转载自马士兵,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论