Java 内存模型 synchronized 的内存语义
synchronized 具有使每个线程依次排队操作共享变量的功能。这种同步机制效率很低,但 synchronized 是其它并发容器实现的基础。
一、锁对象及 synchronized 的使用
synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码。
锁对象分为两种:
实例对象(一个类有多个)和 Class 对象(一个类只有一个)。
不同锁对象之间的代码执行互不干扰,同一个类中加锁方法与不加锁方法执行互不干扰。
使用 synchronized 也有两种方式:
修饰普通方法,锁当前实例对象。修饰静态方法,锁当前类的 Class 对象。
修饰代码块,锁括号中的对象(实例对象或 Class 对象)。

class Xz { // 类锁 public static synchronized void aa() { for (int i = 0; i < 10; i++) { System.out.println("aaa"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } // 对象锁 public synchronized void bb() { for (int i = 0; i < 10; i++) { System.out.println("bbb"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } // 无锁 public void cc() { for (int i = 0; i < 10; i++) { System.out.println("ccc"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class SynchronizedTest { public static void main(String[] args) { Xz xz = new Xz(); // 执行互不干扰 new Thread(() -> { Xz.aa(); }).start(); new Thread(() -> { xz.bb(); }).start(); new Thread(() -> { xz.cc(); }).start(); } }
二、特性
原子性
被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。
可见性
对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
有序性
synchronized 本身是无法禁止指令重排和处理器优化的。
as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。
编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。
synchronized 修饰的代码,同一时间只能被同一线程执行。所以,可以保证其有序性。
三、synchronized 的实现:monitor 和 ACC_SYNCHRONIZED

package com; /** * 编译:javac com\SynchronizedTest.java * 反编译:javap -v com\SynchronizedTest */ public class SynchronizedTest { public static void main(String[] args) { synchronized (SynchronizedTest.class) { System.out.println("haha!"); } } public synchronized void xx(){ System.out.println("xixi!"); } }
反编译上述代码,结果如下(省去了不相关信息)
{ public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: ldc #2 // class com/SynchronizedTest 2: dup 3: astore_1 4: monitorenter // 获取锁,之后其它要执行该段代码的线程需要等锁释放 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String haha! 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit // 锁内代码执行完毕,释放锁,其他线程可再次获取锁 15: goto 23 18: astore_2 19: aload_1 20: monitorexit // 锁内代码发生异常时自动释放锁 21: aload_2 22: athrow 23: return Exception table: from to target type 5 15 18 any // 5 行至 15 行发生异常时跳至 18 行执行,即锁内代码发生异常时自动释放锁 18 21 18 any // 18 行至 21 行发生异常时跳至 18 行执行 public synchronized void xx(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 线程在执行有 ACC_SYNCHRONIZED 标志的方法时需要先获得锁 Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String xixi! 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 15: 0 line 16: 8 }
同步代码块
JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.14
使用 monitorenter 和 monitorexit 两个指令实现。
每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增(可重入性)。当同一个线程释放锁(执行 monitorexit)后,该计数器自减。当计数器为0的时候,锁将被释放。
同步方法
JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当线程访问时候,会检查是否有 ACC_SYNCHRONIZED,有则需要先获得锁,然后才能执行方法,执行完或执行发生异常都会自动释放锁。
ACC_SYNCHRONIZED 也是基于 Monitor 实现的。
四、Mark Word 与 ObjectMonitor
对象的实例保存在堆上,对象的元数据保存在方法区,对象的引用保存在栈上。
对象的实例在堆中的数据可分为:对象头(包含 Mark Word 和 Klass Pointer)、实例数据、对齐填充(HotSpot 要求对象的起止地址必须是 8 的倍数)。
OOP-KLASS
https://blog.csdn.net/qq_36706941/article/details/111411772
术语:https://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
Class 类在 JVM 中对应的对象文件为 instanceKlass.hpp,数组类在 JVM 中对应的对象文件为 arrayKlass.hpp
HotSpot 采用 instanceOopDesc 和 arrayOopDesc 来描述对象头,arrayOopDesc 对象用来描述数组类型。instanceOopDesc 的定义的在 Hotspot 源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp。
从 instanceOopDesc 代码中可以看到 instanceOopDesc 继承自 oopDesc,oopDesc 的定义在 Hotspot 源码中的 oop.hpp 文件中。
类实例对象在 JVM 中对应的对象文件为 oop.hpp,其中引用了 markOop.hpp,即 Mark Word,Mark Word 文件中又引用了 ObjectMonitor 文件。
UseCompressedOops 会影响对象头的内容,默认是开启的,64 位会被压缩成 32 位。https://stackoverflow.com/questions/60985782/details-about-mark-word-of-java-object-header
Mark Word
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,对象头被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
下图描述了在 32 位虚拟机上,非数组对象在不同状态时 mark word 各个比特位区间的含义。如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
源码中(markOop.hpp)关于对象头对象的定义,主要包含了 GC 分代年龄、锁状态标记、哈希码、epoch(偏向时间戳)等信息。
enum { age_bits = 4, lock_bits = 2, biased_lock_bits = 1, max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 };
源码中(markOop.hpp)关于对象头中锁状态的定义。
enum { locked_value = 0, // 00 轻量级锁 unlocked_value = 1, // 001 无锁 monitor_value = 2, // 10 监视器锁,膨胀锁,重量级锁 marked_value = 3, // 11 GC标记 biased_lock_pattern = 5 // 101 偏向锁 };
ObjectMonitor
源码中(objectMonitor.hpp)关于 Monitor 对象的定义。
ObjectMonitor() { _header = NULL; _count = 0; // 用来记录该线程获取锁的次数 _waiters = 0, _recursions = 0; // 锁的重入次数 _object = NULL; // 存储该 Monitor 的对象(被 synchronized 锁住的那个对象) _owner = NULL; // 指向持有 ObjectMonitor 对象的线程 _WaitSet = NULL; // 存放处于 wait 状态的线程队列(获取到锁后调用了被锁对象 wait 方法) _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 多线程竞争锁时的单向列表,第一次没有获取到锁的线程会进入到这里,第二次还没有获取到锁就会进入 _EntryList 阻塞 FreeNext = NULL ; _EntryList = NULL ; // 存放处于等待锁 block 状态的线程队列 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。
若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒(等待 synchronized 锁的线程不可被中断,即使调用了该线程的 interrupt 方法。只有获取到锁之后才会中断。另外中断操作只是给线程一个标记,最终执行是看线程本身的状态)。
若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁)。
五、OpenJDK 源码
竞争锁
(monitorenter 指令函数)进入 monitor 竞争锁:interpreterRuntime::monitorenter,monitorexit 指令也在此
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); if (UseBiasedLocking) { // 是否使用了偏向锁,这里多个线程竞争,就不会使用偏向锁 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { // 会进入到这里,使用轻量级锁 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); } assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object"); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END
ObjectSynchronizer::slow_enter
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { // 省略一些代码 lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); // 膨胀为重量级锁 } ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
最终调用 ObjectMonitor::enter
void ATTR ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD ; void * cur ; // 通过 CAS 操作尝试把 monitor 的 _owner 字段设置为当前线程 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; return ; } if (cur == Self) { // 线程重入,recursions++ _recursions ++ ; return ; } if (Self->is_lock_owned ((address)cur)) { // 如果当前线程是第一次进入该 monitor,设置 _recursions 为 1,_owner 为当前线程 assert (_recursions == 0, "internal state error"); _recursions = 1 ; _owner = Self ; OwnerIsThread = 1 ; return ; } // 省略一些代码 { 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); } }
此处省略锁的自旋优化等操作。以上代码的具体流程概括如下:
- 通过 CAS 尝试把 monitor 的 owner 字段设置为当前线程。
- 如果设置之前的 owner 指向当前线程,说明当前线程再次进入 monitor,即重入锁,执行 recursions++ ,记录重入的次数。
- 如果当前线程是第一次进入该 monitor,设置 recursions 为 1,_owner 为当前线程,该线程成功获得锁并返回。
- 如果获取锁失败,则等待锁的释放。
等待锁
竞争失败,等待调用。ObjectMonitor::enterI
void ATTR ObjectMonitor::EnterI (TRAPS) { Thread * Self = THREAD ; // 再次尝试抢占锁 if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } // 省略部分代码 // 当前线程被封装成 ObjectWaiter 对象 node,状态设置成 ObjectWaiter::TS_CXQ ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; // 通过 CAS 把 node 节点 push 到 _cxq 列表中 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } } // 省略部分代码 for (;;) { // 线程在被挂起前做一下挣扎,看能不能获取到锁 if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ; if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; Self->_ParkEvent->park() ; // 通过 park 将当前线程挂起,等待被唤醒 } if (TryLock(Self) > 0) break ; // 省略部分代码 } // 省略部分代码 } // 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁 int ObjectMonitor::TryLock (Thread * Self) { for (;;) { void * own = _owner ; if (own != NULL) return 0 ; if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { assert (_recursions == 0, "invariant") ; assert (_owner == Self, "invariant") ; return 1 ; } if (true) return -1 ; } }
以上代码的具体流程概括如下:
- 当前线程被封装成 ObjectWaiter 对象 node,状态设置成 ObjectWaiter::TS_CXQ。
- 在 for 循环中,通过 CAS 把 node 节点 push 到 _cxq 列表中,同一时刻可能有多个线程把自己的 node 节点 push 到 _cxq 列表中。
- node 节点 push 到 _cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过 park 将当前线程挂起,等待被唤醒。
- 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
释放锁
当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码。
在 HotSpot 中,通过退出 monitor 的方式实现锁的释放,并通知被阻塞的线程,具体实现位于 ObjectMonitor 的 exit 方法中:ObjectMonitor::exit
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * Self = THREAD ; // 省略部分代码 if (_recursions != 0) { _recursions--; // 可重入锁,退出同步代码块时会让 _recursions 减 1,当 _recursions 的值减为 0 时,说明线程释放了锁 TEVENT (Inflated exit - recursive) ; return ; } // 省略部分代码 for (;;) { // 省略部分代码 ObjectWaiter * w = NULL ; // 之前被等待线程的包装 int QMode = Knob_QMode ; if (QMode == 2 && _cxq != NULL) { // qmode=2:直接绕过 _EntryList 队列,从 cxq 队列中获取线程用于竞争锁 w = _cxq ; assert (w != NULL, "invariant") ; assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; ExitEpilog (Self, w) ; // 唤醒线程 return ; } if (QMode == 3 && _cxq != NULL) { //qmode=3:cxq 队列插入 _EntryList 尾部 w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } ObjectWaiter * Tail ; for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ; if (Tail == NULL) { _EntryList = w ; } else { Tail->_next = w ; w->_prev = Tail ; } } if (QMode == 4 && _cxq != NULL) { // qmode=4:cxq 队列插入到 _EntryList 头部 w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } if (_EntryList != NULL) { q->_next = _EntryList ; _EntryList->_prev = q ; } _EntryList = w ; } w = _EntryList ; if (w != NULL) { assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ExitEpilog (Self, w) ; return ; } w = _cxq ; if (w == NULL) continue ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } TEVENT (Inflated exit - drain cxq into EntryList) ; assert (w != NULL , "invariant") ; assert (_EntryList == NULL , "invariant") ; if (QMode == 1) { ObjectWaiter * s = NULL ; ObjectWaiter * t = w ; ObjectWaiter * u = NULL ; while (t != NULL) { guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ; t->TState = ObjectWaiter::TS_ENTER ; u = t->_next ; t->_prev = u ; t->_next = s ; s = t; t = u ; } _EntryList = s ; assert (s != NULL, "invariant") ; } else { _EntryList = w ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } } if (_succ != NULL) continue; w = _EntryList ; if (w != NULL) { guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ExitEpilog (Self, w) ; return ; } } } void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { assert (_owner == Self, "invariant") ; _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; ParkEvent * Trigger = Wakee->_event ; Wakee = NULL ; OrderAccess::release_store_ptr (&_owner, NULL) ; OrderAccess::fence() ; if (SafepointSynchronize::do_call_back()) { TEVENT (unpark before SAFEPOINT) ; } DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self); Trigger->unpark() ; // 唤醒之前被 pack() 挂起的线程 if (ObjectMonitor::_sync_Parks != NULL) { ObjectMonitor::_sync_Parks->inc() ; } } // 被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI(TRAPS),继续执行 monitor 的竞争 void ATTR ObjectMonitor::EnterI (TRAPS) { for (;;) { // park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; Self->_ParkEvent->park() ; } if (TryLock(Self) > 0) break ; // 省略部分代码 } // 省略部分代码 }
根据不同的策略(由 QMode 指定),从 cxq 或 EntryList 中获取头节点,通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由 unpark 完成
六、monitor 是重量级锁
可以看到 ObjectMonitor 的函数调用中会涉及到 Atomic::cmpxchg_ptr、Atomic::inc_ptr 等内核函数,执行同步代码块,没有竞争到锁的对象会 park() 被挂起,竞争到锁的线程会 unpark() 唤醒。
这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以 synchronized 是 Java 语言中是一个重量级(Heavyweight)的操作。
用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下 Linux 系统的体系架构:
从上图可以看出,Linux 操作系统的体系架构分为:
用户空间(应用程序的活动空间)和内核。
内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括 CPU 资源、存储资源、I/O 资源等。
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。
所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O 调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。系统调用的过程可以简单理解为:
- 用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。
- 用户态程序执行系统调用。
- CPU 切换到内核态,并跳到位于内存指定位置的指令。
- 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
- 系统调用完成后,操作系统会重置 CPU 为用户态并返回系统调用的结果。
由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在 synchronized 未优化之前,效率低的原因
https://www.hollischuang.com/archives/2637
https://www.hollischuang.com/archives/tag/深入理解多线程
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://www.cnblogs.com/dennyzhangdd/p/6734638.html
https://juejin.im/post/5d5374076fb9a06ac76da894
https://hunterzhao.io/post/2018/02/24/hotspot-explore-java-object-model-oop-klass
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构