以下内容大多是学习链接,他人整理,个人收藏以便复习,同时归纳分享出来(如有不妥,原作者可随时联系本人删除,感谢!)
二、Java并发
2、深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)
深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)-InfoQ
深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(下)
深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下)-InfoQ
AQS源码图解:【深入AQS原理】我画了35张图就是为了让你深入 AQS - 掘金 (juejin.cn)
图解AQS的设计与实现,手摸手带你实现一把互斥锁! - Java填坑笔记 - 博客园 (cnblogs.com)
(视频: {2021最新}java并发编程中最难的AQS框架源码解析-【视频教程全集】_哔哩哔哩_bilibili
【Java架构跟我学】深入理解并发编程AbstractQueuedSynchronizer源码解读,找我免费一对一辅导_哔哩哔哩_bilibili
Java并发锁框架AQS(AbstractQueuedSynchronizer)原理从理论到源码透彻解析_哔哩哔哩_bilibili )
3、ConcurrentHashMap源码(1.7和1.8)
ConcurrentHashMap 源码浅析 1.7_pjcdpainful的技术博客_51CTO博客(1.7)
jdk1.7分段锁的实现
和hashmap一样,在jdk1.7中ConcurrentHashMap的底层数据结构是数组加链表。和hashmap不同的是ConcurrentHashMap中存放的数据是一段段的,即由多个Segment(段)组成的。每个Segment中都有着类似于数组加链表的结构。
关于Segment
ConcurrentHashMap有3个参数:
initialCapacity:初始总容量,默认16
loadFactor:加载因子,默认0.75
concurrencyLevel:并发级别,默认16
其中并发级别控制了Segment的个数,在一个ConcurrentHashMap创建后Segment的个数是不能变的,扩容过程过改变的是每个Segment的大小。
关于分段锁
段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段,当每个Segment越来越大时,锁的粒度就变得有些大了。
分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步synchronized是有优势的。
缺点在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当某个段很大时,分段锁的性能会下降。
jdk 1.8中ConcurrentHashmap采用的底层数据结构为数组+链表+红黑树的形式。数组可以扩容,链表可以转化为红黑树。
采用的是 Synchronized + CAS ,把锁的粒度进一步降低,而放弃了 Segment 分段。(此时的 Synchronized 已经升级了,效率得到了很大提升,锁升级可以了解一下)
为什么不用ReentrantLock而用synchronized ?
减少内存开销:如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
内部优化:synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。
在 1.8 HashMap 中的线程安全问题,就是因为在多个线程同时操作同一个桶的头结点时,会发生值的覆盖情况。那么,顺着这个思路,我们看一下在 ConcurrentHashmap中它是怎么避免这种情况发生的吧:
ConcurrentHashMap源码分析(1.8) - Ouka傅 - 博客园(1.8源码详解)
ConcurrentHashMap底层实现原理(JDK1.7 & 1.8) - 简书(1.7和1.8大概思路)
ConcurrentHashMap源码分析(JDK8版本)_惟愿无事-CSDN博客_concurrenthashmap源码分析
ConcurrentHashMap1.8 - 扩容详解_ZOKEKAI的博客-CSDN博客_concurrenthashmap扩容(流程带图)
4、Condition.await, signal 与 Object.wait, notify 的区别
Condition.await, signal 与 Object.wait, notify 的区别_qq_16257883的博客-CSDN博客
详解Condition的await和signal等待/通知机制 - 简书
5、sleep() 和 wait() 的区别
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。 因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
sleep方法中源码注释:
The thread
* does not lose ownership of any monitors.
sleep() 和 wait() 的区别_yinhuanxu-CSDN博客_sleep和wait
6、Doug Lea在J.U.C包里面写的BUG又被网友发现了(较难,可不作为基础知识扩展学习)
Doug Lea在J.U.C包里面写的BUG又被网友发现了 - why技术 - 博客园
7、why技术博客(推荐阅读)
8、LockSupport的用法及原理
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
park和unpark的使用不会出现死锁的情况(stop和resume如果顺序反了,会出现死锁现象)
优点:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
-
其实park/unpark的设计原理核心是“许可”:park是等待一个许可,unpark是为某线程提供一个许可。
如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。 -
有一点比较难理解的,是unpark操作可以再park操作之前。
也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。这其实是必须的。考虑最简单的生产者(Producer)消费者(Consumer)模型:Consumer需要消费一个资源,于是调用park操作等待;Producer则生产资源,然后调用unpark给予Consumer使用的许可。非常有可能的一种情况是,Producer先生产,这时候Consumer可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。那么等Consumer准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。
作者:SinX竟然被占用了
链接:https://www.jianshu.com/p/e3afe8ab8364
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
自己动手写把”锁”---LockSupport深入浅出 - 清泉^_^ - 博客园
10、不可不说的Java“锁”事(美团技术博客)
11、为什么需要可重入锁?
就是一个方法里,外层方法占用了锁,但是里面还有方法要获得锁,如果不是重入锁,程序无法继续运行,陷入死锁,是重入锁就可以继续执行。
12、Java线程池实现原理及其在美团业务中的实践(美团技术博客):
(1)总体设计:
线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl
这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
如下是线程任务队列:
private final BlockingQueue<Runnable> workQueue;
execute线程的时候,如果线程达到核心数,调用workQueue.offer方法,非阻塞添加;
取的时候,直接获取workQueue.poll(),或者阻塞获取workQueue.take():
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
................
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
(2)生命周期:
(3)、任务执行机制
a、任务调度:所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务
b、任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
c、任务申请:
由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。
线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:
d、任务拒绝:
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
(4)Worker线程管理
a、Worker线程
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。
线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。
b、Worker线程增加
增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:
c、 Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
d、Worker线程执行任务
在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:
1.while循环不断地通过getTask()方法获取任务。 2.getTask()方法从阻塞队列中取任务。 3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。 4.执行任务。 5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队(美团技术博客,推荐)
源码分析: 线程池ThreadPoolExecutor的学习 - 汪神 - 博客园
13、从ReentrantLock的实现看AQS的原理及应用:(美团技术博客)
从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队
14、Java内存访问重排序的研究 (美团技术博客)
15、分布式队列编程优化篇 (美团技术博客)
16、【Java 线程池】Java 创建线程池的正确姿势: Executors 和 ThreadPoolExecutor 详解:
(避免使用无界阻塞队列,并且使用guava包给线程池命名,方便排查问题)
【Java 线程池】Java 创建线程池的正确姿势: Executors 和 ThreadPoolExecutor 详解 - 云+社区 - 腾讯云
17、并发容器-ConcurrentLinkedQueue详解
并发容器-ConcurrentLinkedQueue详解 - 简书
18、线程池submit和execute方法有什么区别
线程池提交任务的两种方式:execute与submit的区别 - 莫等、闲 - 博客园
19、一个线程池中的线程异常了,那么线程池会怎么处理这个线程?
【原创】有的线程它死了,于是它变成一道面试题。 - why技术 - 博客园
20、线程的状态:
java.lang.Thread类详解 - Ryan520 - 博客园
21、为什么start方法会调用run方法?(为什么不能直接run来实现创建线程?)
Thead类中start()方法和run()方法的区别
start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程,通过Thead类中start()方法来启动的线程处于就绪状态(可运行状态),此时并没有运行,一旦得到CPU时间片,就自动开始执行run()方法。此时不需要等待run()方法执行完也可以继续执行下面的代码,所以也由此看出run()方法并没有实现多线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
为什么start方法会调用run方法?(为什么不能直接run来实现创建线程?)_吴成伟的博客-CSDN博客
22、sleep() 和 wait() 的区别
sleep() 和 wait() 的区别_yinhuanxu-CSDN博客_sleep和wait
23、Thread中stop(),suspend(),resume()为什么不安全
Thread中stop(),suspend(),resume()为什么不安全 - 简书
24、java 为什么wait(),notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?
假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。 点睛之笔
阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用_萧萧的专栏-CSDN博客
25、线程池中submit() 和 execute()方法有什么区别?
Java线程池的submit和execute方法区别 - boonya的个人页面 - OSCHINA - 中文开源技术交流社区
java面试题之java中用到的线程调度算法是什么 - 胡金水 - 博客园
27、Java多线程循环打印ABC的5种实现方法
Java多线程循环打印ABC的5种实现方法_XDarker的博客-CSDN博客_多线程循环打印abc
28、深入理解Java线程池:ScheduledThreadPoolExecutor
深入理解Java线程池:ScheduledThreadPoolExecutor - 简书
29、队列:PriorityQueue
PriorityQueue是一种无界的,线程不安全,通过数组实现,并拥有优先级的队列,存储的元素要求必须是可比较的对象, 如果不是就必须明确指定比较器Comparator
30、队列:PriorityBlockingQueue
线程安全,采用比较器Comparator排序(排优先级,用的堆,就是一个二叉树)
【Java并发编程】阻塞队列PriorityBlockingQueue实现原理及源码解析_fxkcsdn的博客-CSDN博客_priorityblockingqueue原理
31、ArrayBlockingQueue 与 LinkedBlockingQueue 比较 :
入队:
offer(E e):如果队列没满,立即返回true; 如果队列满了,立即返回false-->不阻塞
put(E e):如果队列满了,一直阻塞,直到队列不满了或者线程被中断-->阻塞
offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果队列已满,则进入等待,直到出现以下三种情况:-->阻塞
被唤醒
等待时间超时
当前线程被中断
出队:
poll():如果没有元素,直接返回null;如果有元素,出队
take():如果队列空了,一直阻塞,直到队列不为空或者线程被中断-->阻塞
poll(long timeout, TimeUnit unit):如果队列不空,出队;如果队列已空且已经超时,返回null;如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:
被唤醒
等待时间超时
当前线程被中断
ArrayBlockingQueue:
一个对象数组+一把锁+两个条件;
入队与出队都用同一把锁;
在连续入队高并发或连续出队高并发的情况下,因为操作数组,且不需要扩容,性能很高;
采用了数组,必须指定大小,即容量有限;
LinkedBlockingQueue:
一个单向链表+两把锁+两个条件
两把锁,一把用于入队,一把用于出队,有效的避免了入队与出队时使用一把锁带来的竞争;
在入队与出队都高并发的情况下,性能比ArrayBlockingQueue高很多;
采用了链表,最大容量,默认为整数最大值,也可以在构造方法时候指定(否则会有内存溢出风险);
【JUC】JDK1.8源码分析之LinkedBlockingQueue(四) - leesf - 博客园
【JUC】JDK1.8源码分析之ArrayBlockingQueue(三) - leesf - 博客园
32、DelayedWorkQueue实现延时队列:
Java优先级队列DelayedWorkQueue原理分析 - 简书
33、如何用LinkedHashmap实现LRU:
LinkedHashMap实现LRU - 附重点源码解析 - 云+社区 - 腾讯云
34、面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存(如果不用LinkedHashmap)
阿里面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存,我懵了! - 知乎
35、wait为什么要在同步块中使用? 为什么sleep就不用再同步块中?
假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。
wait为什么要在同步块中使用? 为什么sleep就不用再同步块中? - myseries - 博客园
36、Java--偏向锁/轻量级锁/重量级锁
Java--偏向锁/轻量级锁/重量级锁_lwgzj的博客-CSDN博客
37、深入理解Java并发之synchronized实现原理
Synchronized关键字深析(小白慎入,深入jvm源码,两万字长文)_Java新生代-CSDN博客
深入理解Java并发之synchronized实现原理_zejian的博客-CSDN博客_synchronized原理
38、锁的等级:方法锁、对象锁、类锁
39、怎么检测一个线程是否拥有锁 :java Thread holdsLock()方法检测一个线程是否拥有锁
java holdsLock()方法检测一个线程是否拥有锁_小小布的程序世界-CSDN博客
40、Executors类是什么? Executor和Executors的区别
Executor、ExecutorService 和 Executors 三者的继承关系 和 不同点_那年那些事儿-CSDN博客_线程池继承关系
41、几种不同线程池的场景:
42、分布式队列编程:模型、实战(美团技术博客)
43、分布式队列编程优化篇(美团技术博客)
44、Thread.yield() 方法的作用:
yield 使当前线程让出 CPU 时间片,线程从运行状态(Running)变为可执行状态(Runnable),处于可执行状态的线程有可能会再次获取到时间片继续执行,也有可能处于等待状态,直到再次获取到时间片。也就是说,后续会有两种情况:
(1)、当前线程让出 CPU 时间片后,又立即获取到 CPU 时间片,进而继续执行当前方法。
(2)、当前线程让出 CPU 时间片后,等待一段时间后获取到 CPU 时间片,进而继续执行当前方法。
(ConcurrentHashMap在初始化table的时候,调用initTable()方法时,采用了Thread.yield()方法,
//如果一个线程发现sizeCtl<0,意味着另外的线程执行CAS操作成功,当前线程只需要让出cpu时间片)
45、CAS理解:
Java并发编程-无锁CAS与Unsafe类及其并发包Atomic_zejian的博客-CSDN博客
46、CAS中的ABA问题:
(1). 独占锁:
属于悲观锁,有共享资源,需要加锁时,会以独占锁的方式导致其它需要获取锁才能执行的线程挂起,等待持有锁的钱程释放锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁的思想。
(2)乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观锁一般会使用版本号机制或CAS算法实现。
(3)就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
(4)什么是CAS:
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
(5)CAS中的ABA问题:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
(6)解决CAS中的ABA问题:AtomicStampedReference
死磕 java并发包之AtomicStampedReference源码分析(ABA问题详解) - 知乎
47、给线程池重命名:
Java线程池中三种方式创建 ThreadFactory 设置线程名称_阿飞云-CSDN博客_threadfactory
线程池---友好的线程池命名_yueloveme的博客-CSDN博客_线程池给线程命名
48、AtomicLong中的方法,调用的unsafe.getAndAddLong,在高并发时,一直do while ,效率比较低:
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
49、替代AtomicLong并发高的方案:LongAdder(少量线程,AtomicLong性能高于LongAdder,高并发时LongAdder性能更好,LongAdder以空间换时间思想)
(对于Java项目中计数统计的一些需求,如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数))
(AtomicLong是多个线程针对单个热点值value进行原子操作。而LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作)
LongAdder在多个线程更新一个用于收集统计信息的而不是追求同步的公共和的情况下,是优于AtomicLong类的。在并发度小,低竞争情况下,两个类具有相似的性能。但是在高争用情况下,LongAdder的预期吞吐量要高得多,代价是更高的空间消耗。
sum方法返回值只是一个接近值,并不是一个准确值。它在计算总和时,并发的更新并不会被合并在内。
总结:
- LongAdder是一种以空间换时间的解决方案,其在高并发,竞争大的情况下性能更优。
- 但是,sum方法拿到的只是接近值,追求最终一致性。如果业务场景追求高精度,高准确性,用AtomicLong。
视频:JDK8 新特性LongAdder源码深度讲解,保证让你学到很多硬核知识!_哔哩哔哩_bilibili
上述视频笔记:新闻-面试官问我LongAdder,我惊了...
博客:https://segmentfault.com/a/1190000015865714
50、
51、volatile在JMM中的语义
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这
些单个读/写操作做了同步
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement () { // 普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
如上面示例程序所示,一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都
是使用同一个锁来同步,它们之间的执行效果相同。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对
一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
锁的语义决定了临界区代码的执行具有原子性。
从JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。
从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和
锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义
·当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保
volatile写之前的操作不会被编译器重排序到volatile写之后。
·当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保
volatile读之后的操作不会被编译器重排序到volatile读之前。
·当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
52、java内存模型JMM:
53、cas底层实现原理:
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读 / 写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低 lock 前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
- unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
- Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性