通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!
一、实现网站访问计数器
1、线程不安全的做法
1.1、代码
package com.chentongwei.concurrency; import static java.lang.Thread.sleep; /**
* @Description:
* @Project concurrency
*/
public class TestCount { private static int count; public void incrCount() {
count ++;
} public static void main(String[] args) throws InterruptedException {
TestCount testCount = new TestCount();
// 开启五个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 每个线程都让count自增100
for (int j = 0; j < 100; j++) {
testCount.incrCount();
}
}).start();
}
sleep(2000);
// 正确的情况下会输出500
System.out.println(count);
}
}
1.2、结果
并不一定是500,极大可能小于500。不固定。
1.3、分析
很明显上面那段程序是线程不安全的,为什么线程不安全?因为++操作其实是类似如下的两步骤,如下:
count ++;
||
// 获取count
int temp = count;
// 自增count
count = temp + 1;
很明显是先获取在自增,那么问题来了,我线程A和线程B都读取到了int temp = count;
这一步,然后都进行了自增操作,其实这时候就错了因为这时候count丢了1,并发了。所以导致了线程不安全,结果小于等于500。
2、Synchronized保证线程安全
2.1、代码
package com.chentongwei.concurrency; import static java.lang.Thread.sleep; /**
* @Description:
* @Project concurrency
*/
public class TestCount { private static int count; public void incrCount() {
count ++;
} public static void main(String[] args) throws InterruptedException {
TestCount testCount = new TestCount();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
synchronized (TestCount.class) {
testCount.incrCount();
}
}
}).start();
}
sleep(2000);
System.out.println(count);
}
}
2.2、结果
500
2.3、分析
没什么可分析的,我用了Java的内置锁Synchronized来保证了线程安全性。加了同步锁之后,count自增的操作变成了原子性操作,所以最终输出一定是500。众所周知性能不好,所以继续往下看替代方案。
3、原子类保证线程安全
3.1、代码
package com.chentongwei.concurrency; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.Thread.sleep; /**
* @Description:
* @Project concurrency
*/
public class TestCount { // 原子类
private static AtomicInteger count = new AtomicInteger(); public void incrCount() {
count.getAndIncrement();
} public static void main(String[] args) throws InterruptedException {
TestCount testCount = new TestCount();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
testCount.incrCount();
}
}).start();
}
sleep(2000);
System.out.println(count);
}
}
3.2、结果
500
3.3、分析
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。每个原子类内部都采取了CAS算法来保证的线程安全性。
二、什么是CAS算法
1、概念
CAS的英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
2、原理
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,才将内存值修改为B,否则什么都不做,最后返回现在的V值。
简单理解为这句话:我认为V的值应该是A,如果是A的话我就把他改成B,如果不是A的话(那就证明被别人修改过了),那我就不修改了,避免多人 同时修改导致数据出错。换句话说:要想修改成功,必须保证A和V中的值是一样的,修改前有个对比的过程。
比如:更新一个变量,只有当变量的预期值(A)和内存地址(V)的实际值相同时,才会将内存地址(V)对应的值修改为B。
我们看如下的原理图:
1、在内存地址V当中,存储着值为10的变量。
2、此时线程1想把变量的值增加1,对于线程1来说,旧的预期值A=10,要修改的新值B=11。
3、在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量率先更新成了11。
4、线程1开始提交更新,首先进行A和地址V的实际值对比,发现A!=V,提交失败。
5、线程1重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说:A=11,B=12.这个重新尝试的过程称为自旋。
6、这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
7、线程1进行交换,把地址V的值替换为B,也就是12.
3、对比Synchronized
从思想上来讲,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,高并发情况下效率低下。而CAS属于乐观锁,乐观的认为程序中的并发情况不那么严重,所以让线程不断去重试更新。但实际上Synchronized已经改造了,带有锁升级的功能。效率不亚于cas。
4、CAS缺点
(1)CPU开销可能过大
在并发比较大的时候,若多线程反复尝试更新某个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。(因为是个死循环,下面分析底层实现就懂了。)
(2)不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子操作,而不能保证整个代码块的原子性。比如需要保证三个变量共同进行原子性的更新,就不得不使用Synchronized或Lock等机制了。
(3)ABA问题。
下面会单独抽出一块地来详细讲解。这是CAS最大的漏洞。
三、CAS底层实现(Java)
1、概述
要说Java中CAS的案例,那么最属java.util.concurrent.atomic
包下的原子类有发言权了。最经典、最简单。
2、讲解
比如我们这里随便找个AtomicInteger
来讲解CAS算法底层实现。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
} private volatile int value; public final int get() {
return value;
}
获取当前值
当前值+1,计算出目标值
进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性)。
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
什么是unsafe呢?
3、Unsafe
Unsafe是CAS的核心类,Java语言不像C,C++那样可以直接访问底层操作系统,Java无法直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
而valueOffset是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。
我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
四、ABA问题
1、演示
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A。然后线程1执行CAS时发现变量的值仍是A,所以CAS成功,这么看没毛病,但是如果操作的是个链表呢?那就炸了,因为虽然值一样,但是链表的位置不一样了。
例如:
(1)现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
head.compareAndSet(A,B);
(2)在T1执行上面这条指令(CAS)之前,线程T2介入,将A、B出栈,在push三个D、C、A,如下:
(3)此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,因为B已经再上一步被移除了,成为了游离态。所以此时的情况变为
导致了其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>
也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。
2、生活案例
你和你前任分手后她又回来了,但是你在这期间又和其他女人...,你表面还是你,但是本质的你已经变了。把这个例子带到代码里来就是:
你有个class,里面有个LinkedList属性,这个链表里有你和你前任,你先把它踹了,然后小苍进来跟你...,这时候你前任就回来了,但是这期间链表已经发生了无感知的变化。`
通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!的更多相关文章
- 手摸手带你理解Vue的Computed原理
前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...
- 手摸手带你理解Vue的Watch原理
前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...
- 手摸手带你理解Vue响应式原理
前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它 ...
- 带你理解Lock锁原理
同样是锁,先说说synchronized和lock的区别: synchronized是java关键字,是用c++实现的:而lock是用java类,用java可以实现 synchronized可以锁住代 ...
- [转帖]从零开始入门 K8s | 手把手带你理解 etcd
从零开始入门 K8s | 手把手带你理解 etcd https://zhuanlan.zhihu.com/p/96721097 导读:etcd 是用于共享配置和服务发现的分布式.一致性的 KV 存储系 ...
- JDK1.8源码逐字逐句带你理解LinkedHashMap底层
注意 我希望看这篇的文章的小伙伴如果没有了解过HashMap那么可以先看看我这篇文章:http://blog.csdn.net/u012403290/article/details/65442646, ...
- 2-12-配置squid代理服务器加快网站访问速度
本节所讲内容: squid服务器常见概念 squid服务器安装及相关配置文件 实战:配置squid正向代理服务器 实战:配置透明squid代理提升访问速度 实战:配置squid反向代理加速度内网web ...
- Apache 使用gzip、deflate 压缩页面加快网站访问速度
Apache 使用gzip 压缩页面加快网站访问速度 介绍: 网页压缩来进一步提升网页的浏览速度,它完全不需要任何的成本,只不过是会让您的服务器CPU占用率稍微提升一两个百分点而已或者更少. 原理 ...
- [技术博客]使用CDN加快网站访问速度
[技术博客]使用CDN加快网站访问速度 2s : most users are willing to wait 10s : the limit for keeping the user's atten ...
随机推荐
- (九)HttpClient获取cookies
原文链接:https://blog.csdn.net/cheny1p1ng/article/details/90780024 旧版本DefaultHttpClient 使用getCookieStore ...
- Cookie 和 Session 关系详解
什么是 Cookie 和 Session ? 什么是 Cookie HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在 ...
- 开启PG的归档模式
目录 开启PG的归档模式 1.查看pg的数据目录 2.查看pg的归档情况 3.查看归档的模式和位置 4.建立归档目录 5.配置归档参数 6.重启pg 7.查看&&切换归档日志 8.查看 ...
- AIO,BIO,NIO,IO复用,同步,异步,阻塞和非阻塞
(1)什么是NIO(Non-blocked IO),AIO,BIO (2) 区别 (3)select 与 epoll,poll区别 1.什么是socket?什么是I/O操作? 什么是socket? 实 ...
- WeChair——团队展示
这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 团队名称 WeChair 这个作业要求在哪里 团队作业第一次 这个作业的目标 团队合作,项目开发 作业正文 如下 其他参考文献 ...
- 韩顺刚-tcp报文头协议详细分析第一包数据:序号是0,发送数据的长度是0,因为没有收到对端的数据,所以确认号是0, Syn的标志位设置成1,这里没有发送的数据,只发送TCP的20个字节的头部
TCP报文段首部格式 大部分TCP报文头部都是20个字节,有的数据包要加上选项. 上面一行代表4个字节,源端口和目的端口都是2个字节. TCP协议是面向字节流的协议 TCP是一段一段分块的发送数据的 ...
- 谈谈 Promise 以及实现 Fetch 的思路
Promise 是异步编程的一种解决方案. Promise /** * 属性 */ Promise.length Promise.prototype /** * 方法 */ Promise.all(i ...
- Java | 顶层类(Top-Level Class)
前言 本文内容根据 Java 官方教程中的<课程:类和对象>编写而成. 本文提供的是 JDK 14 的示例代码. 定义 顶层类(Top-Level Class),是 Java 中对类的一种 ...
- 小白—职场之Java基础篇
java基础篇 java基础 目录 1.java是一种什么语言,jdk,jre,jvm三者的区别 2.java 1.5之后的三大版本 3.java跨平台及其原理 4.java 语言的特点 5.什么是字 ...
- "Celsius=5/9*(Fahrenheit-32)" and "Celsius=5*(Fahrenheit-32)/9 "
The reason for multiplying by 5 and dividing by 9 instead of just multiplying by 5/9 is that in C, a ...