写点什么

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

  • 2014-07-23
  • 本文字数:10398 字

    阅读完需:约 34 分钟

JVM 线程 dump Bug 描述

在 JAVA 语言中,当同步块(Synchronized)被多个线程并发访问时,JVM 中会采用基于互斥实现的重量级锁。JVM 最多只允许一个线程持有这把锁,如果其它线程想要获得这把锁就必须处于等待状态,也就是说在同步块被并发访问时,最多只会有一个处于 RUNNABLE 状态的线程持有某把锁,而另外的线程因为竞争不到这把锁而都处于 BLOCKED 状态。然而有些时候我们会发现处于 BLOCKED 状态的线程,它的最上面那一帧在打印其正在等待的锁对象时,居然也会出现 -locked 的信息,这个信息和持有该锁的线程打印出来的结果是一样的 (请看下图),但是对比其他 BLOCKED 态的线程却并没有都出现这种情况。当我们再次 dump 线程时又可能出现不一样的结果。测试表明这可能是一个偶发的情况,本文就是针对这种情况对 JVM 内部的实现做了一个研究以寻找其根源。

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;
}
}
...
}

在这里重点解释下在安全点内执行的 vm_opration 的过程,VMThread 通过不断循环从 _vm_queue 中获取一个或者几个需要在安全点内执行的 vm_opertion,然后在准备执行这些 vm_opration 之前先通过调用 SafepointSynchronize::begin() 进入到安全点状态,在执行完这些 vm_opration 之后,调用 SafepointSynchronize::end(),退出安全点模式,恢复之前暂停的所有线程让他们继续运行。对于安全点这块的逻辑挺复杂的,仅仅需要记住在进入安全点模式的时候会持有 Threads_lock 这把线程互斥锁,对线程的操作都需要获取到这把锁才能继续执行,并且还会设置安全点的状态,如果正在进入安全点过程中设置 _state 为 _synchronizing,当所有线程都完全进入了安全点之后设置 _state 为 _synchronized 状态,退出的时候设置为 _not_synchronized 状态。

复制代码
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 JavaThread::print_stack_on(outputStream* st) {
if (!has_last_Java_frame()) return;
ResourceMark rm;
HandleMark hm;
RegisterMap reg_map(this);
vframe* start_vf = last_java_vframe(®_map);
int count = 0;
for (vframe* f = start_vf; f; f = f->sender() ) {
if (f->is_java_frame()) {
javaVFrame* jvf = javaVFrame::cast(f);
java_lang_Throwable::print_stack_element(st, jvf->method(), jvf->bci());
if (JavaMonitorsInStackTrace) {
jvf->print_lock_info_on(st, count);
}
} else {
// Ignore non-Java frames
}
count++;
if (MaxJavaStackTraceDepth == count) return;
}
}

和我们这次问题相关的逻辑,也就是打印"-locked"的信息是正好是在 jvf->print_lock_info_on(st, count) 这行里面,请看具体实现:

复制代码
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);
}
}
}
}

看到上面的方法,再对比线程 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 位而言,其不同的值代表不同的含义:

复制代码
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 呢?

水落石出

在 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();)
}
...
}

也许我们看到可能造成问题的代码了:

复制代码
if (SafepointSynchronize::do_call_back()) {
SafepointSynchronize::block(thread);
}

想象一下,当这个线程正好执行到这个条件判断,然后进去了,从方法名上来说是不是意味着这个线程会 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 不会对生产环境产生影响,本文主要是和大家分享分析问题的过程,希望大家碰到疑惑都能有一查到底的劲儿,带着问题,不断提出自己的猜想,然后不断验证自己的猜想,最终解决问题。


感谢郭蕾对本文的审校和策划。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-07-23 23:548300

评论

发布
暂无评论
发现更多内容

OceanBase 源码解读(六):存储引擎详解

OceanBase 数据库

数据库 开发者 高性能 资源隔离 租户

springboot集成阿里云短信

小鲍侃java

11月日更

为什么那么多人在用WGCLOUD

王逅逅

zabbix 监控系统 linux运维 运维系统

内在可解释模型之RuleFit

索信达控股

机器学习 算法 模型

技术干货|开源项目-FlyFish使用攻略

云智慧AIOps社区

开源 大前端 低代码 数据可视化 大屏

腾讯安全李滨:腾讯云数据安全与隐私保护探索与实践

腾讯安全云鼎实验室

数据安全 云安全

Apache APISIX 扩展指南

API7.ai 技术团队

Apache 插件 API网关 Apache APISIX

什么是DISA STIG?概述+STIG安全

旋极智能

业务数据清洗,落地实现方案

数据 数据清洗 数据管理 数据服务 业务数据

个人信息保护法生效,企业数据安全合规正当时

行云管家

信息安全 数据安全 企业安全 网络保护

初识Java反射概念和使用

CRMEB

JavaIO流核心模块与基本原理

Java nio IO流 字符流 字节流

入职字节跳动那一天,我哭了(蘑菇街被裁,奋战7个月拿下offer)

Java MySQL redis 程序员 算法

低代码是什么意思?

低代码小观

程序员 低代码 开发工具 开发平台 企业开发系统

原来我才是内卷王,闭关3个月肝完Java 7大核心知识,成功斩获字节58万Offer。

Java高级开发

字节跳动 java; 字节跳动面经

让脂肪起内讧?从内部全面瓦解脂肪

脑极体

百度人脸活体检测系统通过信通院“护脸计划”首批优秀级安全防护能力评估

百度开发者中心

安全 人脸识别 百度安全

一文,动态规划入门

bigsai

算法 动态规划

行云管家荣登36kr企服点评云计算软件排行榜NO.1

行云管家

云计算 软件 排行榜 IT运维

真香!180页100+题15W+字解析的《Java高级面试指南》,果断收下

Java 程序员 架构 分布式 算法

墨天轮国产数据库沙龙 | 黄新著:金仓数据库全生命周期管控

墨天轮

国产数据库 KingBase 人大金仓

如何获取所有安装的应用程序信息

Changing Lin

11月日更

恒源云(GPUSHARE)_Child Tuning: 反向传播版的Dropout

恒源云

深度学习

“神算子”上线!EasyDL时序预测模型零门槛轻松上手

百度开发者中心

百度飞桨

推动产业创新,腾讯的底层逻辑是什么?

ToB行业头条

极光笔记丨Spark SQL 在极光的建设实践

极光GPTBots-极光推送

大数据 spark 计算引擎

300行ABAP代码实现一个最简单的区块链原型

汪子熙

区块链 SAP abap 11月日更

IOS技术分享| WebRTC iOS源码下载&编译

anyRTC开发者

ios 音视频 WebRTC 实时通信 视频直播

白码低代码/无代码开发平台功能及作用

低代码小观

低代码 开发工具 开发平台 无代码 企业服务

《Linux一学就会》:第二章:Linux基本命令操作和文件管理

侠盗安全

Linux 运维 linux运维 云计算架构师

11.11上云嘉年华,华为云数据库助力客户备战业务高峰

华为云数据库小助手

GaussDB GaussDB(for openGauss) GaussDB ( for Redis ) 华为云数据库

JVM Bug:多个线程持有一把锁_JVM_李嘉鹏_InfoQ精选文章