面试被问到原题了!百度和腾讯二面问的两道多线程面试题解析

第一道面试题:线程唤醒问题

样例代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

    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();

    }

}

输出结果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

    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操作的反向操作,所以这里很简单,无非就是将线程从等待集中移出并且唤醒,源码如下:

1

2

3

4

5

    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,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

    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方法来唤醒,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

    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读者都只唤醒一个,很多人都说随机唤醒一个,那么我们这里来看看唤醒算法是什么,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

    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问题

样例代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

    public class ThreadAliveTest {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {

            System.out.println("t1 start");

            try {

                Thread.sleep(2000);

            catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("t1 end");

        });

        t1.start();

        Thread t2 = new Thread(() -> {

            synchronized (t1) {

                System.out.println("t2 start");

                try {

                    Thread.sleep(5000);

                catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("t1 isAlive:" + t1.isAlive());

            }

        });

        t2.start();

    }

}

    输出结果:

    t1 start

t2 start

t1 end

t1 isAlive:true

问题:

为什么线程结束了,isAlive方法还返回true

分析:

我们首先看看执行流程,线程T1启动后将会睡眠2秒,随后2秒后执行结束,随后线程T2启动,T2首先获取到T1的对象锁,然后睡眠5秒,随后调用T1的isAlive方法判定线程是否存活,那么为什么会输出true呢?

我们还得先看看isAlive方法如何实现的。我们来看源码。

1

2

3

4

5

6

7

8

9

10

11

12

13

    public final native boolean isAlive();

    首先看到isAlive方法由JNI方法实现。我们来看Hotspot源码。

    JVM_ENTRY(jboolean, JVM_IsThreadAlive(JNIEnv* env, jobject jthread))

  JVMWrapper("JVM_IsThreadAlive");

  oop thread_oop = JNIHandles::resolve_non_null(jthread);

  return java_lang_Thread::is_alive(thread_oop);

JVM_END

我们看到首先通过resolve_non_null方法将jthread转为oop对象thread_oop,随后调用java_lang_Thread的is_alive方法来判断是否存活,我们继续跟进。

1

bool java_lang_Thread::is_alive(oop java_thread) {  JavaThread* thr = java_lang_Thread::thread(java_thread);  return (thr != NULL);}JavaThread* java_lang_Thread::thread(oop java_thread) {  return (JavaThread*)java_thread->address_field(_eetop_offset);}

我们看到最后是通过获取java thread对象,也即java的Thread类中的eetop属性,如果该属性为null,那么表明线程已经销毁,也即返回false,如果eetop还在那么返回true,表明线程存活。

那么什么是eetop呢?我们还得从线程创建方法入手。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

    JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))

  JVMWrapper("JVM_StartThread");

  JavaThread *native_thread = NULL;

  bool throw_illegal_thread_state = false;      // 非法线程状态标识

  {

    // Threads_lock上锁,保证C++的线程对象和操作系统原生线程不会被清除。当前方法执行完,也就是栈帧释放时,会释放这里的锁,当然肯定会调用析构函数,而这个对象的析构函数中调用unlock方法释放锁

    MutexLocker mu(Threads_lock);

if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) { // 如果线程不为空,则表明线程已经启动,则为非法状态     

throw_illegal_thread_state = true;

else {

  // 本来这里可以检测一下stillborn标记来看看线程是否已经停止,但是由于历史原因,就让线程自己玩了,这里就不玩了

     // 取得线程对象的stackSize的大小

      jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      // 开始创建C++ Thread对象和原生线程对象,使用无符号的线程栈大小,所以这里不会出现负数

      size_t sz = size > 0 ? (size_t) size :0;

     // 创建JavaThread,这里的thread_entry为传入的运行地址,也就是启动线程,需要一个入口执行点,这个函数地址便是入口执行点

      native_thread = new JavaThread(&thread_entry, sz);

     // 如果osthread不为空,则标记当前线程还没有被使用

      if (native_thread->osthread() != NULL) {

        native_thread->prepare(jthread);

      }

    }

  }

  // 如果throw_illegal_thread_state不为0,那么直接抛出异常

if (throw_illegal_thread_state) {

    THROW(vmSymbols::java_lang_IllegalThreadStateException());

  }

  // 原生线程必然不能为空,因为线程是由操作系统创建的,所以没有OS线程,空有个JavaThread类有啥用0.0

  if (native_thread->osthread() == NULL) {

    delete native_thread;       // 直接用C++的delete释放内存

    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),"unable to create new native thread");

  }

  Thread::start(native_thread);     // 一切准备妥当,开始启动线程

JVM_END

我们看到首先创建了JavaThread对象,该对象内部创建了OSThread对象,我们这么理解:JavaThread代表了C++层面的Java线程,而OSThread代表了操作系统层面的线程对象。

随后调用了native_thread->prepare(jthread)方法为启动线程做准备。我们关注该方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

    void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) {

    // 包装当前Java线程对象

    Handle thread_oop(Thread::current(),

                      JNIHandles::resolve_non_null(jni_thread));

    // 将Java层面的线程Oop对象与JavaThread C++层面的对象关联

    set_threadObj(thread_oop());

    java_lang_Thread::set_thread(thread_oop(), this);

    // 设置优先级

    if (prio == NoPriority) {

        prio = java_lang_Thread::priority(thread_oop());

    }

    Thread::set_priority(this, prio);

    // 将JavaThread类放入到全局线程列表中

    Threads::add(this);

}

我们注意看 java_lang_Thread::set_thread方法。我们跟进它的源码。

1

2

3

4

    void java_lang_Thread::set_thread(oop java_thread, JavaThread* thread) {

    // 将JavaThread C++层面的线程对象设置为Java层面的Thread oop对象的eetop变量

    java_thread->address_field_put(_eetop_offset, (address)thread);

}

这下我们知道了eetop变量即是JavaThread对象的地址信息。

在了解完eetop如何被设置之后我们得继续看,eetop什么时候被取消。

当Java线程执行完Runnable接口的run方法最后一个字节码后,将会调用exit方法。

该方法完成线程对象的退出和清理操作,我们重点看ensure_join方法。

1

2

3

4

5

    void JavaThread::exit(bool destroy_vm, ExitType exit_type) {

    ...

    ensure_join(this);

    ...

}

我们继续跟进ensure_join的源码实现。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

    static void ensure_join(JavaThread* thread) {

    // 封装Java Thread线程oop对象

    Handle threadObj(thread, thread->threadObj());

    // 获取Java Thread线程oop对象锁

    ObjectLocker lock(threadObj, thread);

    // 清除未处理的异常信息

    thread->clear_pending_exception();

    // 将状态修改为TERMINATED

    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);

    // 将Java Thread线程oop对象与JavaThread C++对象解绑

    java_lang_Thread::set_thread(threadObj(), NULL);

    // 唤醒所有阻塞在线程对象的线程

    lock.notify_all(thread);

    // 如果以上代码期间发生异常,那么清理挂起的异常

    thread->clear_pending_exception();

}

我们看到最终由ensure_join方法中的

java_lang_Thread::set_thread(threadObj(), NULL),将eetop变量设置为null。

当执行完这一步时,我们再通过isAlive方法判断线程是否存活时,将返回false,否则返回true。

而我们看到在操作该变量时需要获取线程对象锁。我们来看ObjectLocker的构造函数和析构函数的实现。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

    ObjectLocker::ObjectLocker(Handle obj, Thread* thread, bool doLock) {

    _dolock = doLock;

    _thread = thread;

    if (_dolock) {

        // 获取Java Thread线程oop对象锁

        ObjectSynchronizer::fast_enter(_obj, &_lock, false, _thread);

    }

}

ObjectLocker::~ObjectLocker() {

    if (_dolock) {

        // 释放Java Thread线程oop对象锁

        ObjectSynchronizer::fast_exit(_obj(), &_lock, _thread);

    }

}

我们看到当我们创建ObjectLocker对象时,会在构造函数中获取到线程对象锁,而当ensure_join方法执行完毕后,将会调用ObjectLocker的析构函数,在该函数中释放线程对象锁。

总结:

这下我们就可以通过以上知识来分析为何isAlive方法在线程执行完毕后仍然返回true了。

这是用于isAlive方法通过判断Java线程对象的eetop变量来判定线程是否存活,而当我们线程执行完毕后将会调用exit方法,该方法将会调用ensure_join方法,在该方法中将eetop甚至为null。

但是由于赋值前需要获取到Java线程的对象锁,而该对象的对象锁已经由线程T2持有,这时当前线程将会阻塞,从而造成eetop变量没有被清除,从而导致isAlive方法在T1线程执行完毕后仍然返回true。

读者也可以看看java Thread的源码,join函数也是通过对Thread对象获取锁然后调用isAlive来判定线程是否结束的。

这就意味着如果我们用别的线程持有了Java Thread的对象锁,那么这时调用join方法的线程也是会被阻塞的。

想要了解更多,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值