一、有这样一道面试题
面试题目:实现一个 限流场景中 使用的计数器,说出你的方案
二、实现方案
第一种实现方案:直接使用int变量 i++ 操作
计数器最终结果: 17477。<=2000,说明存在多线程安全问题
能用,局限于 非多线程 场景
为什么会出现 多线程下 计算不准确的问题?
java提供的命令:javap -v xx.class 可以帮我们看懂字节码文件
我们可以看到 JAVA在这里的底层操作:1、获取当前值;2、计算;3、赋值 。两个线程同时获取到同一个当前值,提交的时候第一个线程提交,第二个线程提交覆盖第一个线程提交的。
多线程 -- 线程安全 -- 原子性问题
如何保证原子性 ?
操作中多个步骤执行过程中,涉及的核心资源(数据)保持一致。
灵魂拷问:剩下的这两种方案你选哪一种?理由是什么?
第二种实现方案: 加锁(synchronized或lock )
public sychronized int incr()
计数器的结果:20000
锁在这种情况下使用存在的不足:锁在争抢的过程中会让线程进入阻塞的状态,阻塞就意味着需要操作系统下一次调度才能执行。
操作系统调度的两个因素: 1、服务器上线程的数量 --》JAVA服务器尽量不要跑其他软件,线程池控制线程数量 2、其他线程执行的任务类型。有的人回想,若CPU上有一个耗时很长的线程,一直没有CPU释放会出现什么情况?这个问题CPU早就想到了,它的每个线程执行到指定时间后,都会退出进入阻塞状态,和其他线程共同竞争。如果该线程还没执行完成,那就再次进行CPU中执行,循环一直到线程执行完成
第三种实现方案: JDK提供计数器AtomicInteger
AtomicInteger i = new AtomicInteger();
计数器的结果:20000
为什么大家都选这个?CAS ( Compare And Swap )
什么是CAS机制(https://www.cnblogs.com/myopensource/p/8177074.html)
https://blog.csdn.net/qq_32998153/article/details/79529704底层原理机制--》1、最后提交时,先对比检查数据有没有变化 2、如果一致则操作成功。否则重头再来
优点:锁还有CPU重新调度的时间问题,CAS不存在,只是在当前CPU下循环调用
三、分析以上三种方案--方案一
3.1 代码实现
3.2 为什么会出现多线程安全问题
这需要看JAVA底层是怎么操作的。如下图:CPU-0和CPU-1操作后理论上应该是2,但是实际结果有可能是1。
这是因为CPU-0和CPU-1都从内存中读到的i=0,i++后都得到i=1,两次赋值都是1。这就是多线程下面的线程安全问题(原子性没有得到保证)。
3.3 原子性是什么
原子性,我们在数据库事务ACID中应该听到过。数据库使用锁机制来保证原子性。
如何保证原子性 ?
操作中多个步骤执行过程中,涉及的核心资源(数据)保持一致。
四、分析以上三种方案--方案二
4.1 代码实现
4.2 原理图
4.3 线程的状态
NEW:尚未启动的线程的线程状态。
RUNNABLE:可运行线程的线程状态。
可运行线程状态正在Java虚拟机中执行,但它可能正在等待操作系统中的其他资源例如处理器。BLOCKED:等待监视器锁定时阻塞的线程的线程状态。
WAITING:等待线程的线程状态。
TIMED_WAITING:具有指定等待时间的等待线程的线程状态。
TERMINATED(terminated):终止线程的线程状态。线程已完成执行。
=========Java源码里提供了6种线程的状态。但我们在网上搜索线程状态,有五种有七种的,是为什么呢?这是和底层操作系统有关的。为了统一,Java代码实现了6种状态,应用在JVM上,至于JVM和底层操作系统的交互,直接被封装起来了。==========
五、分析以上三种方案--方案三
5.1 代码实现
5.2 原理
5.3 什么是CAS机制