当前位置:天才代写 > tutorial > JAVA 教程 > JVM Bug:多个线程持有一把锁

JVM Bug:多个线程持有一把锁

2017-11-02 08:00 星期四 所属: JAVA 教程 浏览:540

副标题#e#

JVM线程dump Bug描写

在JAVA语言中,当同步块(Synchronized)被多个线程并发会见时,JVM中会回收基于互斥实现的重量级锁。JVM最多只答允一个线程持有这把锁,假如其它线程想要得到这把锁就必需处于期待状态,也就是说在同步块被并发会见时,最多只会有一个处于RUNNABLE状态的线程持有某把锁,而别的的线程因为竞争不到这把锁而都处于BLOCKED状态。然而有些时候我们会发明处于BLOCKED状态的线程,它的最上面那一帧在打印其正在期待的锁工具时,居然也会呈现-locked的信息,这个信息和持有该锁的线程打印出来的功效是一样的(请看下图),可是比拟其他BLOCKED态的线程却并没有都呈现这种环境。当我们再次dump线程时又大概呈现纷歧样的功效。测试表白这大概是一个偶发的环境,本文就是针对这种环境对JVM内部的实现做了一个研究以寻找其来源。

JVM Bug:多个线程持有一把锁

jstack呼吁的整个进程

上面提到了线程dump,那么就不得不提执行线程dump的东西—jstack,这个东西是Java自带的东西,和Java处于同一个目次下,主要是用来dump线程的,或者各人也有利用kill -3的呼吁来dump线程,但这两者最明明的一个区别是,前者的dump内容是由jstack这个历程来输出的,方针JVM历程将dump内容发给jstack历程(留意这是没有加-m参数的场景,指定-m参数就有点纷歧样了,它利用的是serviceability agent的api来实现的,底层通过ptrace的方法来获取方针历程的内容,执行进程大概会比正常模式更长点),这意味着可以做文件重定向,将线程dump内容输出到指定文件里;尔后者是由方针历程输出的,只会发生在方针历程的尺度输出文件里,假如正巧尺度输出里自己就有内容的话,看起来会较量乱,好比想通过一些阐明东西去阐明的话,要是该东西没有做过滤操纵,很大概无法阐明。因此一般环境我们只管利用jstack,别的jstack尚有许多实用的参数,好比jstack pid >thread_dump.log,该呼吁会将指定pid的历程的线程dump到当前目次的thread_dump.log文件里。

jstack是利用Java实现的,它通过给方针JVM历程发送一个threaddump的呼吁,方针JVM的监听线程(attachListener)会及时监听传过来的呼吁(其实attachListener线程并不是一启动就建设的,它是lazy建设启动的),当attachListener收到threaddump呼吁时会挪用thread_dump的要领来处理惩罚dump操纵(要领在attachListener.cpp里)。

static jint thread_dump(AttachOperation* op, outputStream* out) {
  bool print_concurrent_locks = false;
  if (op->arg(0) != NULL && strcmp(op->arg(0), "-l") == 0) {
    print_concurrent_locks = true;
  }

  // thread stacks
  VM_PrintThreads op1(out, print_concurrent_locks);
  VMThread::execute(&op1);

  // JNI global handles
  VM_PrintJNI op2(out);
  VMThread::execute(&op2);

  // Deadlock detection
  VM_FindDeadlocks op3(out);
  VMThread::execute(&op3);

  return JNI_OK;
}

从上面的要领可以看到,jstack呼吁执行了三个操纵:

VM_PrintThreads:打印线程栈

VM_PrintJNI:打印JNI

VM_FindDeadlocks:打印死锁

三个操纵都是交给VMThread线程去执行的,VMThread线程在整个JAVA历程有且只会有一个。可以想象一下VMThread线程的简朴执行进程:不绝地轮询某个任务列表并在有任务时依次执行任务。任务执行时,它会按照详细的任务抉择是否会暂停整个应用,也就是stop the world,这是不是让我们遐想到了我们熟悉的GC进程?是的,我们的ygc以及cmsgc的两个暂停应用的阶段(init_mark和remark)都是由这个线程来执行的,而且都要求暂停整个应用。其实上面的三个操纵都是要求暂停整个应用的,也就是说jstack触发的线程dump进程也是会暂停应用的,只是这个进程一般很快就竣事,不会有明明的感受。别的内存dump的jmap呼吁,也是会暂停整个应用的,假如利用了-F的参数,其底层也是利用serviceability agent的api来dump的,可是dump内存的速度会明明慢许多。

VMThread执行任务的进程

VMThread执行的任务称为vm_opration,在JVM中存在两种vm_opration,一种是需要在安详点内执行的(所谓安详点,就是系统处于一个安详的状态,除了VMThread这个线程可以正常运行之外,其他的线程都必需暂停执行,在这种环境下就可以安心执行当前的一系列vm_opration了),别的一种是不需要在安详点内执行的。而这次我们接头的线程dump是需要在安详点内执行的。

以下是VMThread轮询的逻辑:

void VMThread::loop() {
  assert(_cur_vm_operation == NULL, "no current one should be executing");

  while(true) {
    ...
    //已经获取了一个vm_operation
    if (_cur_vm_operation->evaluate_at_safepoint()) {
        //假如该vm_operation需要在安详点内执行
        _vm_queue->set_drain_list(safepoint_ops); 
        SafepointSynchronize::begin();//进入安详点
        evaluate_operation(_cur_vm_operation);
        do {
          _cur_vm_operation = safepoint_ops;
          if (_cur_vm_operation != NULL) {
            do {
              VM_Operation* next = _cur_vm_operation->next();
              _vm_queue->set_drain_list(next);
              evaluate_operation(_cur_vm_operation);
              _cur_vm_operation = next;
              if (PrintSafepointStatistics) {
                SafepointSynchronize::inc_vmop_coalesced_count();
              }
            } while (_cur_vm_operation != NULL);
          }
          if (_vm_queue->peek_at_safepoint_priority()) {
            MutexLockerEx mu_queue(VMOperationQueue_lock,
                                     Mutex::_no_safepoint_check_flag);
            safepoint_ops = _vm_queue->drain_at_safepoint_priority();
          } else {
            safepoint_ops = NULL;
          }
        } while(safepoint_ops != NULL);
        _vm_queue->set_drain_list(NULL);
        SafepointSynchronize::end();//退出安详点
      } else {  // not a safepoint operation
        if (TraceLongCompiles) {
          elapsedTimer t;
          t.start();
          evaluate_operation(_cur_vm_operation);
          t.stop();
          double secs = t.seconds();
          if (secs * 1e3 > LongCompileThreshold) {
            tty->print_cr("vm %s: %3.7f secs]", _cur_vm_operation->name(), secs);
          }
        } else {
          evaluate_operation(_cur_vm_operation);
        }
        _cur_vm_operation = NULL;
      }
    }
    ...
  }

#p#分页标题#e#

在这里重点表明下在安详点内执行的vm_opration的进程,VMThread通过不绝轮回从_vm_queue中获取一个可能几个需要在安详点内执行的vm_opertion,然后在筹备执行这些vm_opration之前先通过挪用SafepointSynchronize::begin()进入到安详点状态,在执行完这些vm_opration之后,挪用SafepointSynchronize::end(),退出安详点模式,规复之前暂停的所有线程让他们继承运行。对付安详点这块的逻辑挺巨大的,仅仅需要记着在进入安详点模式的时候会持有Threads_lock这把线程互斥锁,对线程的操纵都需要获取到这把锁才气继承执行,而且还会配置安详点的状态,假如正在进入安详点进程中配置_state为_synchronizing,当所有线程都完全进入了安详点之后配置_state为_synchronized状态,退出的时候配置为_not_synchronized状态。


#p#副标题#e#

void SafepointSynchronize::begin() {
  ...
  Threads_lock->lock();
  ...
  _state            = _synchronizing;
  ...
   _state = _synchronized;
...
}

void SafepointSynchronize::end() {
    assert(Threads_lock->owned_by_self(), "must hold Threads_lock");
    ...
    _state = _not_synchronized;
    ...
    Threads_lock->unlock();
  }

线程dump中的VM_PrintThreads进程

回到开头提到的JVM线程Dump时的Bug,从我们打印的功效来看也根基猜到了这个进程:遍历每个Java线程,然后再遍历每一帧,打印该帧的一些信息(包罗类,要领名,行数等),在打印完每一帧之后然后打印这帧已经关联了的锁信息,下面代码就是打印每个线程的进程:

/*
void javaVFrame::print_lock_info_on(outputStream* st, int frame_count) {
  ResourceMark rm;
  if (frame_count == 0) {
    if (method()->name() == vmSymbols::wait_name() &&
        instanceKlass::cast(method()->method_holder())->name() == vmSymbols::java_lang_Object()) {
      StackValueCollection* locs = locals();
      if (!locs->is_empty()) {
        StackValue* sv = locs->at(0);
        if (sv->type() == T_OBJECT) {
          Handle o = locs->at(0)->get_obj();
          print_locked_object_class_name(st, o, "waiting on");
        }
      }
    } else if (thread()->current_park_blocker() != NULL) {
      oop obj = thread()->current_park_blocker();
      Klass* k = Klass::cast(obj->klass());
      st->print_cr("\t- %s <" INTPTR_FORMAT "> (a %s)", "parking to wait for ", (address)obj, k->external_name());
    }
  }

  GrowableArray<MonitorInfo*>* mons = monitors();
  if (!mons->is_empty()) {
    bool found_first_monitor = false;
    for (int index = (mons->length()-1); index >= 0; index--) {
      MonitorInfo* monitor = mons->at(index);
      if (monitor->eliminated() && is_compiled_frame()) {
        if (monitor->owner_is_scalar_replaced()) {
          Klass* k = Klass::cast(monitor->owner_klass());
          st->print("\t- eliminated <owner is scalar replaced> (a %s)", k->external_name());
        } else {
          oop obj = monitor->owner();
          if (obj != NULL) {
            print_locked_object_class_name(st, obj, "eliminated");
          }
        }
        continue;
      }
      if (monitor->owner() != NULL) {
        const char *lock_state = "locked";
        if (!found_first_monitor && frame_count == 0) {
          markOop mark = monitor->owner()->mark();
          if (mark->has_monitor() &&
              mark->monitor() == thread()->current_pending_monitor()) {
            lock_state = "waiting to lock";
          }
        }
        found_first_monitor = true;
        print_locked_object_class_name(st, monitor->owner(), lock_state);
      }
    }
  }
}

#p#分页标题#e#

看到上面的要领,再比拟线程dump的功效,我们会发明许多熟悉的对象,好比waiting on,parking to wait for,locked,waiting to lock,并且也清楚了它们别离是在什么环境下会打印的。

那为什么我们的例子中BLOCKED状态的线程本应该打印waiting to lock,可是为什么却打印了locked呢,那说明if (mark->has_monitor() && mark->monitor() == thread()->current_pending_monitor()) 这个条件必定不创立,那这个在什么环境下不创立呢?在验证此问题前,有须要先相识下markOop是什么对象,它是用来干什么的?

markOop是什么

markOop描写了一个工具(也包罗了Class)的状态信息,Java语法层面的每个工具可能Class在JVM的布局暗示中城市包括一个markOop作为Header,虽然尚有一些其他的JVM数据布局也用它做Header。markOop由32位可能64位组成,详细位数按照运行情况而定。

下面的布局图包括markOop每一位所代表的寄义,markOop的值按照所描写的工具的范例(好比是锁工具照旧正常的工具)以及浸染的差异而差异。就算在同一个工具里,它的值也是大概会不绝变革的,好比锁工具,在一开始建设的时候其实并不知道是锁工具,会当成一个正常工具来建设(在工具的范例并没有配置方向锁的环境下,其markOop值大概是0x1),可是跟着我们执行到synchronized的代码逻辑时,就知道其实它是一个锁工具了,它的值就不再是0x1了,而是一个新的值,该值是对应栈帧布局里的监控工具列内外的某一个内存地点。

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

就最后的3位而言,其差异的值代表差异的寄义:

#p#副标题#e#

 enum { locked_value             = 0,//00
         unlocked_value           = 1,//01
         monitor_value            = 2,//10
         marked_value             = 3,//11      
         biased_lock_pattern      = 5 //101
  };

上面的判定条件“mark->has_monitor()”其实就是判定最后的2位是不是10,假如是,则说明这个工具是一个监控工具,可以通过mark->monitor()要领获取到对应的布局体:

  bool has_monitor() const {
    return ((value() & monitor_value) != 0);
  }
  ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }

将一个普通工具转换为一个monitor工具的进程(就是替换markOop的值)请参考为ObjectSynchronizer::inflate要领,能进入到该要领说明该锁为重量级锁,也就是说这把锁其实是被多个线程竞争的。

相识了markOop之后,还要相识下上面谁人条件里的thread()->current_pending_monitor(),也就是这个值是什么时候配置进去的呢?

线程配置期待的监控工具的机缘

配置的逻辑在ObjectMonitor::enter里,要害代码如下:

...
{
    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
    DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
    if (JvmtiExport::should_post_monitor_contended_enter()) {
      JvmtiExport::post_monitor_contended_enter(jt, this);
    }
    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);
    Self->set_current_pending_monitor(this);//配置当前monitor工具为当前线程期待的monitor工具
    for (;;) {
      jt->set_suspend_equivalent();
      EnterI (THREAD) ;
      if (!ExitSuspendEquivalent(jt)) break ;
          _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }
 ... 

配置当前线程期待的monitorObject是在有中文注释的那一行配置的,那么呈现Bug的原因是不是正亏得配置之前举办了线程dump呢?

水落石出

#p#分页标题#e#

在JVM中只会有一个处于RUNNBALE状态的线程,也就是说别的一个打印"-locked"信息的线程是处于BLOCKED状态的。上面的第一行代码:

JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

找到其实现位置:

 JavaThreadBlockedOnMonitorEnterState(JavaThread *java_thread, ObjectMonitor *obj_m) :
    JavaThreadStatusChanger(java_thread) {
    assert((java_thread != NULL), "Java thread should not be null here");
    _active = false;
    if (is_alive() && ServiceUtil::visible_oop((oop)obj_m->object()) && obj_m->contentions() > 0) {
      _stat = java_thread->get_thread_stat();
      _active = contended_enter_begin(java_thread);//要害处
    }
  }

 static bool contended_enter_begin(JavaThread *java_thread) {
    set_thread_status(java_thread, java_lang_Thread::BLOCKED_ON_MONITOR_ENTER);//要害处
    ThreadStatistics* stat = java_thread->get_thread_stat();
    stat->contended_enter();
    bool active = ThreadService::is_thread_monitoring_contention();
    if (active) {
      stat->contended_enter_begin();
    }
    return active;
  } 

上面的contended_enter_begin要了解配置java线程的状态为java_lang_Thread::BLOCKED_ON_MONITOR_ENTER,而线程dump时按照这个状态打印的功效如下:

const char* java_lang_Thread::thread_status_name(oop java_thread) {
  assert(JDK_Version::is_gte_jdk15x_version() && _thread_status_offset != 0, "Must have thread status");
  ThreadStatus status = (java_lang_Thread::ThreadStatus)java_thread->int_field(_thread_status_offset);
  switch (status) {
    case NEW                      : return "NEW";
    case RUNNABLE                 : return "RUNNABLE";
    case SLEEPING                 : return "TIMED_WAITING (sleeping)";
    case IN_OBJECT_WAIT           : return "WAITING (on object monitor)";
    case IN_OBJECT_WAIT_TIMED     : return "TIMED_WAITING (on object monitor)";
    case PARKED                   : return "WAITING (parking)";
    case PARKED_TIMED             : return "TIMED_WAITING (parking)";
    case BLOCKED_ON_MONITOR_ENTER : return "BLOCKED (on object monitor)";
    case TERMINATED               : return "TERMINATED";
    default                       : return "UNKNOWN";
  };
}

正好对应我们dump日志中的信息"BLOCKED (on object monitor)" 也就是说这行代码被正常执行了,那问题就大概出在JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this)和Self->set_current_pending_monitor(this)这两行代码之间的逻辑里了:

    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
    DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
    if (JvmtiExport::should_post_monitor_contended_enter()) {
      JvmtiExport::post_monitor_contended_enter(jt, this);
    }
    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);
    Self->set_current_pending_monitor(this);//配置当前monitor工具为当前线程期待的monitor工具

于是查抄每一行的实现,前面几行都根基可以解除了,因为它们都是很简朴的操纵,下面来阐明下ThreadBlockInVM tbivm(jt)这一行的实现:

 ThreadBlockInVM(JavaThread *thread)
  : ThreadStateTransition(thread) {
    thread->frame_anchor()->make_walkable(thread);
    trans_and_fence(_thread_in_vm, _thread_blocked);
  }

 void trans_and_fence(JavaThreadState from, JavaThreadState to) { 
    transition_and_fence(_thread, from, to); 
 }

 static inline void transition_and_fence(JavaThread *thread, JavaThreadState from, JavaThreadState to) {
    assert(thread->thread_state() == from, "coming from wrong thread state");
    assert((from & 1) == 0 && (to & 1) == 0, "odd numbers are transitions states");
    thread->set_thread_state((JavaThreadState)(from + 1));
    if (os::is_MP()) {
      if (UseMembar) {
        OrderAccess::fence();
      } else {
        InterfaceSupport::serialize_memory(thread);
      }
    }

    if (SafepointSynchronize::do_call_back()) {
      SafepointSynchronize::block(thread);
    }
    thread->set_thread_state(to);
    CHECK_UNHANDLED_OOPS_ONLY(thread->clear_unhandled_oops();)
  }
 ...
 } 

也许我们看到大概造成问题的代码了:

#p#副标题#e#

if (SafepointSynchronize::do_call_back()) {
      SafepointSynchronize::block(thread);
    }

#p#分页标题#e#

想象一下,当这个线程正好执行到这个条件判定,然后进去了,从要领名上来说是不是意味着这个线程会block住,而且不往后走了呢?这样一来配置当前线程的pending_monitor工具的操纵就不会被执行了,从而在打印这个线程栈的时候就会打印"-locked"信息了,那么纠结是否正如我们想的那样呢?

首先来看条件SafepointSynchronize::do_call_back()是否必然会创立:

inline static bool do_call_back() {
    return (_state != _not_synchronized);
  }

上面的VMThread执行任务的进程中说到了这个状态,当vmThread执行完了SafepointSynchronize::begin()之后,这个状态是配置为_synchronized的。假如正在执行,那么状态是_synchronizing,因此,当我们触发了jvm的线程dump之后,VMThread执行该操纵,并且还在执行线程dump进程前,可是还只是_synchronizing的状态,那么do_call_back()将会返回true,那么将执行接下来的SafepointSynchronize::block(thread)要领:

void SafepointSynchronize::block(JavaThread *thread) {
  assert(thread != NULL, "thread must be set");
  assert(thread->is_Java_thread(), "not a Java thread");

  ttyLocker::break_tty_lock_for_safepoint(os::current_thread_id());

  if (thread->is_terminated()) {
     thread->block_if_vm_exited();
     return;
  }

  JavaThreadState state = thread->thread_state();
  thread->frame_anchor()->make_walkable(thread);

  switch(state) {
    case _thread_in_vm_trans:
    case _thread_in_Java:        // From compiled code
      thread->set_thread_state(_thread_in_vm);

      if (is_synchronizing()) {
         Atomic::inc (&TryingToBlock) ;
      }
      Safepoint_lock->lock_without_safepoint_check();
      if (is_synchronizing()) {
        assert(_waiting_to_block > 0, "sanity check");
        _waiting_to_block--;
        thread->safepoint_state()->set_has_called_back(true);

        DEBUG_ONLY(thread->set_visited_for_critical_count(true));
        if (thread->in_critical()) {
          increment_jni_active_count();
        }
        if (_waiting_to_block == 0) {
          Safepoint_lock->notify_all();
        }
      }
      thread->set_thread_state(_thread_blocked);
      Safepoint_lock->unlock();
      Threads_lock->lock_without_safepoint_check();//要害代码
      thread->set_thread_state(state);
      Threads_lock->unlock();
      break;
   ...
  }
  if (state != _thread_blocked_trans &&
      state != _thread_in_vm_trans &&
      thread->has_special_runtime_exit_condition()) {
    thread->handle_special_runtime_exit_condition(
      !thread->is_at_poll_safepoint() && (state != _thread_in_native_trans));
  }
}

void Monitor::lock_without_safepoint_check (Thread * Self) {
  assert (_owner != Self, "invariant") ;
  ILock (Self) ;
  assert (_owner == NULL, "invariant");
  set_owner (Self);
}

void Monitor::lock_without_safepoint_check () {
  lock_without_safepoint_check (Thread::current()) ;
}

看到上面的实现可以确定,Java线程执行时会挪用Threads_lock->lock_without_safepoint_check(),而Threads_lock因为被VMThread持有,将一直卡死在ILock (Self)这个逻辑里,从而没有配置current_monitor属性,由此验证了我们的想法。

Bug修复

在相识了原因之后,我们可以简朴的修复这个Bug。将下面两行代码变更下位置即可:

 ThreadBlockInVM tbivm(jt);
 Self->set_current_pending_monitor(this);//配置当前monitor工具为当前线程期待的monitor工具

该Bug不会对出产情况发生影响,本文主要是和各人分享阐明问题的进程,但愿各人遇到迷惑都能有一查到底的劲儿,带着问题,不绝提出本身的意料,然后不绝验证本身的意料,最终办理问题。

 

    关键字:

天才代写-代写联系方式