资讯动态

Synchronized锁升级与ConcurrentHashMap源码解析

发布时间:2026/10/15 3:42:59 来源:尧图企业网站定制
Synchronized 基础Java内存模型JMMJMM是Java内存模型Java Memory Model用来规定多线程下线程、工作内存、主内存之间的数据访问规则该规范将内存区域分为主内存与工作内存两块形式如下主内存中存储所有实例对象、静态变量多个线程共享硬件上对应RAM内存工作内存是每个线程私有的内存副本线程只能操作该副本不能直接操作主内存硬件上对应CPU缓存、寄存器线程操作内存的流程是读取主内存 拷贝副本到当前线程工作内存 线程读副本修改修改工作内存里的副本 后续刷回主内存设计思想是用CPU高速缓存减少延迟这种模型提高性能的同时也带来多线程的三大核心问题也是Synchronized 主要解决的问题原子性多个线程并行执行操作导致数据脏写可见性一个线程修改了工作内存后未及时刷新回主内存导致其他线程无法感知修改。可用volatile有序性CPU/JVM为了提高性能会对代码执行顺序进行重新调整该调整不影响单线程多线程会导致顺序错乱。可用volatile禁止指令重排Java对象布局一个Java对象在堆内存分为三部分对象头、实例数据、对齐填充。对象头又分为Mark Word和klass pointer。Mark Word用于存储对象自身的运行时数据如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。示例如下Klass Pointer是类型指针是对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。对齐填充要保证对象长度是8的整数倍。有关详细介绍可见Java对象的内存布局Synchronized 特性—互斥、可重入与不可中断互斥性同一时刻只允许一个线程持有锁其他竞争线程阻塞等待解决原子性问题。可重入锁指已经获取到锁的同一线程可再次获取锁不会被自己阻塞该设计允许锁线程调用其他需持有锁的线程示例代码如下publicclassReentrantBlockDemo{publicstaticvoidmain(String[]args){ObjectlocknewObject();newThread(()-{synchronized(lock){// 第一次获取锁count1System.out.println(第一层锁);synchronized(lock){// 再次获取同一把lockcount2System.out.println(第二层锁重入成功);}// count1}// count0锁释放}).start();}}synchronized底层通过Mark Word 记录持有锁的线程ID维护锁计数器。获取一次锁1释放一次-1计数器为0时释放锁。其他线程获取锁时通过线程ID 判断不匹配则阻塞。不可中断阻塞在等待队列的线程不会被interrupt中断示例代码如下publicclassSyncUnInterruptDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ObjectlocknewObject();// 线程1先抢占锁永不释放锁Threadt1newThread(()-{synchronized(lock){System.out.println(t1拿到锁一直持有不释放);while(true){// 死循环不释放锁}}});t1.start();Thread.sleep(500);// 保证t1先拿到锁// 线程2尝试去竞争同一把锁会阻塞等待Threadt2newThread(()-{System.out.println(t2准备抢锁进入synchronized等待);synchronized(lock){System.out.println(t2成功拿到锁);}// 离开同步块后代码可以检查中断标记System.out.println(t2 中断标记Thread.currentThread().isInterrupted());});t2.start();Thread.sleep(500);System.out.println(主线程调用 t2.interrupt());t2.interrupt();// 发起中断Thread.sleep(2000);System.out.println(主线程结束t2仍然在等锁没有被中断唤醒);}}ReentrantLock的lockInterruptibly和tryLock方法可被中断收到 interrupt 中断信号会立刻停止等待Synchronized 与lockJava中的锁体分为内置锁 synchronized和API 层面的显式锁 Lock两种synchronized JVM 提供的关键字底层是监视器锁 ObjectMonitor存在对象头 Mark Word 里。特性互斥、可重入、可见性、有序性锁等待时不可中断。语法上可以修饰实例方法锁 this、静态方法锁 Class 对象、代码块自定义锁对象。锁是自动释放代码正常结束或者抛出异常JVM 自动解锁。锁升级优化分为偏向锁 轻量级锁 重量级锁markdown校验线程id一致直接进入临界区自旋等待不阻塞自旋失败升级未重量级锁锁只能升级默认是非公平锁。配套的等待通知机制是wait/notify/notifyAll只有一个等待队列唤醒的时候没法精准唤醒线程。Lock锁它是一个接口最常用实现类是ReentrantLock底层基于 AQS用 CAS 双向队列实现。属于显式锁需要手动lock()加锁、unlock()释放unlock 必须写在 finally 里不然异常时容易死锁。它的灵活性更强创建时可以选择公平锁或者非公平锁支持lockInterruptibly()等待锁过程可以响应中断tryLock()支持尝试获取锁还支持超时获取避免线程无限阻塞。并且它可以创建多个Condition条件变量多个等待队列能够精准唤醒指定条件的线程。ReentrantLock 也是可重入锁。对比项synchronizedLock类型关键字接口(java.util.concurrent.locks.Lock)释放方式自动释放代码块/方法执行完毕或抛出异常手动释放必须调用unlock()建议放在finally块中断锁等待阶段不可中断支持可中断获取锁lockInterruptibly()锁范围可修饰方法、代码块只能在代码块中编码实现语法层面锁类型默认非公平锁无法手动切换公平/非公平支持公平锁、非公平锁创建时可控锁状态判断无法判断线程是否持有锁tryLock()可尝试获取锁判断是否拿到锁读写分离不支持ReentrantReadWriteLock读写锁读可并发提升读多场景效率锁升级过程JDK1.6以前synchronized使用操作系统的mutex互斥原语实现锁机制多个线程竞争锁时未获得锁的线程会被操作系统挂起进入阻塞队列锁释放后操作系统再唤醒阻塞线程。这种方式要在内核态和用户态反复切换开销较大此时的synchronized锁是重量级锁后续开发者进行了调研发现锁的竞争场景有以下结论多数锁并无竞争长期仅由一个线程持有偏向锁多线程访问锁的竞争并不激烈大部分交替持有锁且锁持有时间较短轻量级锁自旋等待synchronized 整体是混合锁模型偏向锁、轻量级锁是乐观思想的实现只有膨胀后的重量级锁是悲观锁。偏向锁多数锁并无竞争长期仅由一个线程持有首先偏向于第一个获得锁的线程减少同一线程重复获取锁的开销若无偏向锁每次线程进入同步块都要用CAS检查偏向锁只检查线程ID 代替CAS操作在无竞争场景下大幅优化性能偏向锁依赖对象头的Mark Word 标记获取过程如下线程访问同步块检查对象头的Mark Word标记偏向标记位为 1、锁标记 01检查对比偏向线程ID 线程ID为空对象第一次被访问使用CAS将当前线程ID写入Mark Word修改标志位成功后进入临界区线程ID 当前线程ID重入不执行CAS直接进入临界区线程ID ! 当前线程ID锁竞争偏向锁撤销进入安全点检查原线程退出同步块升级为轻量级锁还在临界区执行直接膨胀为重量级锁安全点Safepoint指线程运行到特定位置后堆栈、寄存器状态是确定、可保存的JVM可安全暂停该线程为避免堆栈信息错乱所以要等待线程执行到安全点后JVM才判断线程是否持有锁偏向锁不会主动解除线程退出同步块不会清空Mark Word里的线程ID只有出现其他锁竞争才出发撤销。轻量锁大多数情况锁由多线程交替持有线程持有锁的时间通常较短尽量在用户态CAS而非进入内核态阻塞挂起线程轻量锁加锁流程线程进入同步块在栈帧创建锁记录Lock Record存放Mark Word副本DMW 标记置换字备份加锁前的记录线程尝试CAS操作将对象Mark Word的轻量级锁指针指向自身Lock RecordCAS成功获取轻量级锁修改Mark Word轻量级锁标记进入临界区CAS失败其他线程持有锁进行自旋重试抢锁自旋默认尝试十次自适应自旋锁由前一次在同一个锁上的自旋时间决定自旋抢锁失败升级为重量级锁对象 Mark Word 改为指向操作系统 mutex 指针标记位10当前线程进入内核阻塞队列等待。优化思路JVM 编译器优化锁升级以外即时编译器JIT也会对锁进行优化可以消除无意义的锁锁粗化一段代码多次对同一个对象加锁、释放锁JIT 把相邻的多次加/解锁合并扩大锁的范围减少次数。// 优化前循环内每次迭代都要加锁、释放锁1000次加解锁for(inti0;i1000;i){synchronized(lock){doWork();}}// 优化后锁提到循环外面只加锁1次释放1次synchronized(lock){for(inti0;i1000;i){doWork();}}锁消除分析对象的作用域若对象根本不会被其他线程访问直接删除加锁逻辑最终生成的机器码完全没有锁操作。publicvoidsayHello(){// lock是方法内局部对象不会逃出这个方法其他线程看不到它ObjectlocknewObject();synchronized(lock){System.out.println(hello);}}开发优化减少范围锁内只保留必要的共享对象非必要操作移出同步块缩短锁持有时间。降低粒度全局大锁拆分成独立小锁提高并发度。读写分离读锁共享写锁排他。ConcurrentHashMap源码ConcurrentHashMap是线程安全版的HashMap也是Synchronized的一次实际应用HashMap增加数据的过程为初始化桶算出桶下标桶下标为空直接放入新节点桶头就是目标key直接覆盖桶是红黑树插入数据桶是链表循环遍历找到key就覆盖找不到追加尾部链表长度到8时触发树判断新增元素更新modCount、size超过阈值触发扩容。以put为例源码如下finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){HashMap.NodeK,V[]tab;HashMap.NodeK,Vp;intn,i;// 判断为空则初始化if((tabtable)null||(ntab.length)0)n(tabresize()).length;// 计算桶下标桶为空直接新建节点放入桶中只保存节点// (n - 1) hash 等价hash % n位运算更快if((ptab[i(n-1)hash])null)tab[i]newNode(hash,key,value,null);else{HashMap.NodeK,Ve;Kk;// 头节点是keyif(p.hashhash((kp.key)key||(key!nullkey.equals(k))))ep;// 头节点是红黑树触发插入逻辑elseif(pinstanceofHashMap.TreeNode)e((HashMap.TreeNodeK,V)p).putTreeVal(this,tab,hash,key,value);else{// 链表则遍历寻找for(intbinCount0;;binCount){// 遍历结束未找到增加到尾部节点if((ep.next)null){p.nextnewNode(hash,key,value,null);// 触发树化判断if(binCountTREEIFY_THRESHOLD-1)// -1 for 1sttreeifyBin(tab,hash);break;}// 找到相同key跳出循环执行后续赋值逻辑if(e.hashhash((ke.key)key||(key!nullkey.equals(k))))break;pe;}}// 执行覆盖逻辑只赋值查找和赋值解耦if(e!null){// existing mapping for keyVoldValuee.value;if(!onlyIfAbsent||oldValuenull)e.valuevalue;// 回调钩子方法LinkedHashMap用于维护访问顺序HashMap空实现afterNodeAccess(e);returnoldValue;}}// 修改次数1modCount;// 元素数量超过阈值触发扩容if(sizethreshold)resize();// 插入后钩子LinkedHashMap用来删除最久未访问节点HashMap空实现afterNodeInsertion(evict);returnnull;}这种方式在多线程下不安全如多线程同时put导致数据丢失size统计不准等。ConcurrentHashMap增加了CAS和synchronized 锁桶节点多线程协助扩容分段计数以增加数据为例过程为检查key、value为null直接抛npe计算hash的扰动无限自旋循环读取哈希数组tab未初始化调用initTableCAS保证只有一个线程完成安全点1 下标状态可见计算桶下标 i用 tabAt (volatile 读避免缓存旧值) 获取桶头节点 f为空则写入安全点2 原子写入新节点桶为空则执行 casTabAt 写入成功跳出循环若桶头 hash 为 MOVED代表正在扩容执行 helpTransfer 协助迁移数据更新 tab 后继续循环安全点3 锁住桶的头节点桶为内存地址只能锁对象 桶已有正常节点对桶头 f 加 synchronized 锁加锁后二次校验桶头不变桶头 hash≥0为普通链表遍历链表找到 key 则按需覆盖 value未找到则尾部追加新节点统计 binCount桶是 TreeBin 红黑树调用 putTreeVal 查找 / 插入节点找到 key 按需覆盖 value退出同步块binCount 达到树化阈值执行 treeifyBin 做树化判断存在旧值直接返回 oldVal 并结束调用 addCount 更新元素数量分段 CAS 更新计数内部判断触发扩容。示例代码如下finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){// key 或 value为空抛异常if(keynull||valuenull)thrownewNullPointerException();inthashspread(key.hashCode());intbinCount0;// 无限自旋尝试putfor(ConcurrentHashMap.NodeK,V[]tabtable;;){ConcurrentHashMap.NodeK,Vf;intn,i,fh;// 为空执行初始化if(tabnull||(ntab.length)0)// CAS抢占锁保证只有一个线程初始化成功不用Synchronized是因为此时没有对象tabinitTable();// 安全点1 计算桶下标itabAt是volatile读取桶头节点保证可见性elseif((ftabAt(tab,i(n-1)hash))null){// 安全点2 尝试写入 桶为空则使用casTabAt原子写入新节点成功则跳出自旋if(casTabAt(tab,i,null,newConcurrentHashMap.NodeK,V(hash,key,value,null)))break;// no lock when adding to empty bin}// 正在扩容则帮助迁移elseif((fhf.hash)MOVED)tabhelpTransfer(tab,f);else{// 位置已有节点执行链表或红黑树逻辑VoldValnull;// 锁住桶的头节点桶粒度锁不同桶可并发写入// 业务上等于锁住桶但数组下标是内存synchronized 只能锁对象synchronized(f){// 二次校验判断桶头if(tabAt(tab,i)f){// 普通链表if(fh0){binCount1;// 桶节点数量// 遍历链表for(ConcurrentHashMap.NodeK,Vef;;binCount){Kek;// 找到相同key的覆盖valueif(e.hashhash((eke.key)key||(ek!nullkey.equals(ek)))){oldVale.val;// 保存旧值用于最后返回if(!onlyIfAbsent)//onlyIfAbsent key不存在才写入e.valvalue;break;}ConcurrentHashMap.NodeK,Vprede;// 遍历到尾部增加节点if((ee.next)null){pred.nextnewConcurrentHashMap.NodeK,V(hash,key,value,null);break;}}}// 红黑树插入elseif(finstanceofConcurrentHashMap.TreeBin){ConcurrentHashMap.NodeK,Vp;binCount2;if((p((ConcurrentHashMap.TreeBinK,V)f).putTreeVal(hash,key,value))!null){oldValp.val;if(!onlyIfAbsent)p.valvalue;}}}}// 不为0说明发生写入判断是否需要树化if(binCount!0){// TREEIFY_THRESHOLD8进行树化判断if(binCountTREEIFY_THRESHOLD)// 内部判断数组元素 64时先扩容支持多线程// 64才进行树化treeifyBin(tab,i);if(oldVal!null)returnoldVal;break;}}}// 原子更新元素数量addCount(1L,binCount);returnnull;}协助迁移和树化判断的逻辑比较复杂后面有时间再更finalNodeK,V[]helpTransfer(NodeK,V[]tab,NodeK,Vf){NodeK,V[]nextTab;intsc;// 校验table不为空当前节点是ForwardingNode并且新table nextTable不为nullif(tab!null(finstanceofForwardingNode)(nextTab((ForwardingNodeK,V)f).nextTable)!null){// resizeStamp根据原table长度生成扩容标记左移RESIZE_STAMP_SHIFT构造rsintrsresizeStamp(tab.length)RESIZE_STAMP_SHIFT;// 循环条件nextTab不变、底层table没变、sizeCtl 0代表正在扩容中while(nextTabnextTabletabletab(scsizeCtl)0){// sc rs MAX_RESIZERS扩容线程数量已经达到上限不能再加人帮忙// sc rs 1扩容即将结束rs1是扩容完成前最后状态不需要新线程加入// transferIndex 0所有桶位已经分配完毕没有任务可以迁移if(scrsMAX_RESIZERS||scrs1||transferIndex0)break;// CAS 修改 sizeCtl把扩容线程数 1成功代表当前线程抢到扩容任务if(U.compareAndSwapInt(this,SIZECTL,sc,sc1)){// 执行迁移当前线程协助扩容transfer(tab,nextTab);break;}}// 返回新的tablenextTablereturnnextTab;}// 校验失败直接返回当前tablereturntable;}总结本次学习了Java中的锁synchronized 和 Lock 以及分析了源码中使用synchronized 保证线程安全的容器ConcurrentHashMap。对两种锁的简单总结为synchronized 编码简单自动释放JVM 优化锁升级但是灵活性不足没有超时、中断、多条件队列这些能力Lock 灵活性高精细控制锁行为但编码麻烦必须手动释放需要自己处理异常。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑