|并发进阶高频:CAS、锁升级)
并发进阶前言常见的锁策略乐观锁 VS 悲观锁重量级锁 VS 轻量级锁自旋锁 VS 挂起等待锁读写锁可重入锁 VS 不可重入锁公平锁 VS 非公平锁synchronized 锁的策略CAS认识CAS应用CAS实现原子类实现自旋锁CAS的ABA问题synchronized 原理锁升级锁消除锁粗化全文总结 系列文章导航前言本篇承接上一篇《四大并发实战单例、阻塞队列、定时器与线程池》深挖 Java 并发底层锁机制与无锁 CAS 编程覆盖面试必考的各类锁区分、CAS 原子操作、synchronized 三大优化锁升级 / 消除 / 粗化全部配套伪代码 可运行实战案例吃透底层原理类面试核心考点。常见的锁策略乐观锁 VS 悲观锁这是“锁的一种特性”。此处的悲观和乐观是对后续锁冲突是否激烈给出的预测。乐观锁预测接下来锁冲突的概率不大就可以少做一些工作。悲观锁预测接下来锁冲突的概率很大就应该多做一些工作。重量级锁 VS 轻量级锁重量级锁锁的开销比较大。轻量级锁锁的开销比较小。乐观锁通常是轻量级的锁悲观锁通常是重量级的锁。自旋锁 VS 挂起等待锁自旋锁一种轻量级锁的典型实现。1往往在纯用户态实现。2比如一个while循环不停检查当前锁是否被释放若没有就继续循环释放了就获取到锁从而结束循环。忙等消耗CPU换来更快的响应速度。挂起等待锁一种重量级锁的典型实现。1要借助系统API实现。2一旦出现锁竞争就会在内核中触发一系列的动作比如让这个线程进入阻塞状态暂时不参与CPU调度。阻塞的开销很大。读写锁读写锁把加锁分成两种读加锁、写加锁读加锁读的时候可以读但是不可以写。写加锁写的时候不可以读也不可以写。两个线程加锁过程中1读锁和读锁之间不会产生竞争2读锁和写锁之间有竞争3写锁和写锁之间有竞争。可重入锁 VS 不可重入锁可重入锁一个线程针对同一把锁连续加锁两次不会死锁。不可重入锁一个线程针对同一把锁连续加锁两次会死锁。公平锁 VS 非公平锁当很多线程尝试加同一把锁时一个线程能够拿到锁其他线程阻塞等待一旦第一个线程释放锁之后接下来哪个线程能够拿到锁公平锁按照先来后到的顺序。非公平锁剩下的线程以均等的概率来重新竞争锁。操作系统提供的加锁API默认是非公平锁。synchronized 锁的策略乐观锁 VS 悲观锁自适应轻量级锁 VS 重量级锁自适应自旋锁 VS 挂起等待锁自适应。自适应1初始情况下synchronized会预测当前的锁冲突的概率不大此时以乐观锁模式运行轻量级锁基于自旋锁的方式实现2在实际使用过程中如果发现锁冲突的情况比较多synchronized就会升级成悲观锁重量级锁基于挂起等待的方式实现不是读写锁是可重入锁是非公平锁CAS认识CASCASCompare and swap比较交换的是内存和寄存器。比如有一个内存M两个寄存器ABCAS(M,A,B)如果M和A的值相同就把M和B里的值进行交换同时整个操作返回true否则无事发生同时整个操作返回false交换的本质是为了把B赋值给M。CAS其实是一个CPU指令。单个CPU指令是原子的就可以使用CAS完成一些操作进一步替代加锁。基于CAS实现线程安全的方式为“无锁编程”。应用CAS实现原子类publicclassDemo{publicstaticAtomicIntegercountnewAtomicInteger(0);publicstaticvoidmain(String[]args)throwsInterruptedException{// TODO 自动生成的方法存根Threadt1newThread(()-{for(inti0;i50000;i){count.getAndIncrement();//count}});Threadt2newThread(()-{for(inti0;i50000;i){count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}}原子类里面是基于CAS实现的。伪代码实现classAtomicInteger{privateintvalue;publicintgetAndIncrement(){intoldValuevalue;while(CAS(value,oldValue,oldValue1)!true){//这里的判定就是在判断是否有别的线程穿插过来oldValuevalue;}returnoldValue;}}加锁是通过阻塞的方式避免穿插执行CAS是通过重试的方式避免执行实现自旋锁伪代码publicclassSpinLock{privateThreadownernull;publicvoidlock(){// 通过 CAS 看当前锁是否被某个线程持有.// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.while(!CAS(this.owner,null,Thread.currentThread())){}}publicvoidunlock(){this.ownernull;}}CAS的ABA问题CAS进行操作的关键通过值“没有发生变化”来作为“没有其他线程穿插执行”的判定依据但这种判定方式不严谨极端情况下可能会有另一个线程穿插进来发生将值从A-B-A针对第一个线程虽然值没变但是实际上已经被穿插执行。解决方法让判定的数值按照一个方向增长不要反复横跳有增有减就会发生ABA。引入一个额外的变量版本号约定每次修改版本号就自增一次此时在使用CAS判定时就不是判定值了而是判定版本号看版本号是否变化了若版本号没变就代表没有线程穿插执行。synchronized 原理锁升级synchronized的状态变化无锁 - 偏向锁 - 自旋锁(轻量级锁) - 重量级锁。锁升级的过程是单向的不能再降级了。偏向锁不是真正加锁只是做了一个标记完全是运行时的优化策略。当锁冲突出现时偏向锁就升级成轻量锁就真正加锁了。锁升级的过程就是在性能和线程安全之间尽量进行权衡。锁消除编译器会自动针对当前写的加锁的代码做出判定如果编译器觉得这个场景不需要加锁此时就会把写的synchronized优化掉。例如StringBuilder 不带synchronizedStringBuffer 带有synchronized写了synchronized也不一定线程安全若在单个线程中使用StringBuffer编译器就会把synchronized优化掉编译器只会在自己非常有把握时才进行锁消除锁消除编译期锁消除运行时锁消除。保守保留锁编译器针对synchronized锁的处理策略。核心编译期无法预判运行时的线程竞争情况为了保证程序正确性不会擅自删除或修改代码逻辑只会把锁的语义完整保留到字节码中锁粗化锁的粒度synchronized中代码越少就认为锁的粒度越粗代码越少锁的粒度越细。for(...){sync(lock){n;}//锁粒度细}sync(lock){for(...){n;}//锁粒度粗}锁的粒度细的时候能够并发执行的逻辑更多更有利于充分利用CPU资源。若粒度细的锁被反复进行加锁解锁可能实际效果还不如粒度粗的锁涉及到频繁的锁竞争。全文总结本文完整梳理并发底层核心面试考点六类锁策略区分掌握不同锁适用业务场景CAS 无锁编程底层 CPU 指令原理、原子类实战、手写自旋锁以及 ABA 问题解决方案synchronized 底层三大核心优化单向锁升级、编译期锁消除、锁粗化理解 JVM 锁性能优化逻辑。后续会更新 JUC 工具类、线程安全集合全套实战内容欢迎点赞收藏评论区交流面试学习心得 系列文章导航本篇是「Java并发编程系列」的连载内容点击链接查看完整系列 上一篇Java 并发编程五四大并发实战单例、阻塞队列、定时器与线程池 点击直达「Java并发编程」专栏合集