MySQL中truncate函数自动转型的精度问题

truncate函数

TRUNCATE(X,D) 是MySQL自带的一个系统函数。
其中,X是数值,D是保留小数的位数。
其作用就是按照小数位数,进行数值截取(此处的截取是按保留位数直接进行截取,没有四舍五入)。

现在有这样一个需求,某个表有一个varchar类型的字段,字段内存了一串浮点数。 现在我想要截取到该字段的小数点后两位。

得到的结果毫无疑问,应该是5.02,结果也确实为5.02

那下面这条SQL的结果,你能猜到么?

你可能会说,这还不简单,肯定还是5.02啊。

可是,神奇的一幕出现了,该SQL的执行结果为5.01。 为什么会少了0.1呢,这时就要说到MySQL的TRUNCATE函数做了什么。 因为TRUNCATE函数会对varchar类型做自动转型,产生了精度损失。 在转型后得到的结果其实是5.019999999。然后再做截取,那么结果自然是5.01了。

下面来执行这条SQL

由于没有使用varchar类型,所以TRUNCATE函数没有做自动转型操作,所以并没有产生精度损失,结果为5.02.

cast函数

cast是一种数据类型转换的函数,函数将任何类型的值转换为具有指定类型的值,语法格式如下所示:

CAST ( expression  AS  data_type)

  • expression:任何有效的MySQL表达式或者一些字符串数据。
  • AS:用于分隔两个参数,在AS之前的是要处理的数据,AS之后是要转换的数据类型。
  • data_type:系统所提供的数据类型,这里不能使用用户定义的数据类型。
  • MySQL所能使用的可以是以下类型之一:CHAR(字符型)、DATE(日期)、TIME(时间)、DATETIME(日期时间型)、DECIMAL(浮点数 float)、SIGNED(整数 int)。

依然用5.02这个数字举例:

看似没什么问题,结果也确实是5.02。 但如果使用的是5.029呢?

你会发现,结果变成5.03了,没错,四舍五入了。 执行出来的结果和truncate函数的结果不尽相同,所以也并不能完美满足一开始提出的需求。

总结

这个问题产生的根本问题是使用了varchar类型来存浮点数,并且还需要在数据库内做运算导致的。
我认为解决这个问题的最好时机是在设计时,使用浮点类型来存储该字段。

0

Redis Cluster集群功能测试与踩坑分享

前言

写这篇文章的契机是这样的,现就职公司的Redis是直接购买阿里的云服务,阿里的云服务大家都懂,三个字总结就是好而贵, 虽然云服务内部一定是做了高可用的,但对于使用者(开发人员)来说,这些都是隐形的,是透明的。云服务中的Redis是个黑盒,它甚至仅借用了Redis的概念, 内部是如何实现的,我们一概不知。

所以我对此产生了浓厚的兴趣,我想知道原生Redis集群可以做到何种程度,为的是一旦未来从阿里云迁移到自建服务,至少需要对集群心理有底。

Redis Cluster 与 Sentinel

关于Redis Sentinel和Redis Cluster的问题,是一个会消失还是需要结合使用,下面将进行介绍。

Sentinel

Redis supports multiple slaves replicating data from a master node. This provides a backup node which has your data on it, ready to serve data. However, in order to provide automated failover you need some tool. For Redis this tool is called Sentinel.

Redis支持多个从节点从主节点复制数据。这提供了一个备份节点,上面有您的数据,可以随时提供数据。 但是,为了提供自动故障转移,您需要一些工具。对于Redis,此工具称为Sentinel。 简单的说Sentinel就是一个failover(故障转移)的工具。

它的主要功能有以下几点:

  • 不时地监控redis是否按照预期良好地运行;
  • 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
  • 能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

Sentinel本身也支持集群,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后,sentinel本身也有单点问题。所以有必要将sentinel集群,这样有几个好处:

  • 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题)。
  • 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。
  • sentinel集群自身也需要多数机制,也就是2个sentinel进程时,挂掉一个另一个就不可用了。

Redis Cluster

The use cases for Cluster evolve around either spreading out load (specifically writes) and surpassing single-instance memory capabilities. If you have 2T of data, do not want to write sharding code in your access code, but have a library which supports Cluster then you probably want Redis Cluster. If you have a high write volume to a wide range of keys and your client library supports Cluster, Cluster will also be a good fit.

Cluster的用例围绕分散负载(特别是写入)和超越单实例内存功能而发展。如果您有2T的数据,并且不想在访问代码中编写分片代码,但是拥有一个支持Cluster的库, 那么您可能需要Redis Cluster。如果对大量键的写入量很高,并且客户端库支持Cluster,那么Cluster也将非常适合。

cluster的功能有已下几点:

  • 一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个键都属于这 16384 个哈希槽的其中一个,集群中的每个节点负责处理一部分哈希槽。 例如一个集群有三个节点,其中:
    节点 A 负责处理 0 号至 5500 号哈希槽。
    节点 B 负责处理 5501 号至 11000 号哈希槽。
    节点 C 负责处理 11001 号至 16384 号哈希槽。
    这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。例如:
    如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。
    如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。
  • Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。
  • Redis 集群的节点间通过Gossip协议通信。

对比总结

如果内存需求超过了系统内存,或者需要在多个节点之间分配写操作以保持性能水平,那么使用Redis Cluster。如果寻求高可用,则需要更多地部署Sentinel。 对于是否需要结合使用,答案是不需要,Redis Cluster集群自带failover。使用Cluster时不需要再用Sentinel。

集群部署

这里使用的是docker-compose启动的4主4从集群。
自动草稿

可用性测试

主从切换

先使用docker exec -it node-80 redis-cli -p 6380 cluster info命令查看集群状态

自动草稿

从上图可以看出集群状态显示为ok,说明集群在正常运转。

再使用docker exec -it node-80 redis-cli -p 6380 cluster nodes命令查看各节点状态

自动草稿

从上图可以看出各节点的主从关系,80和85为主从关系,85为主,80为从。

下面插入5条数据:

自动草稿

由上图可以看出数据已经成功插入了。
然后,我将node-85节点停掉。

自动草稿

由上图可以看到80已经从slave节点升级成了master节点,而且集群的状态依然为ok。

接着测试用客户端取集群内80和85分片上的数据。

自动草稿

结果也是ok的,可以查到对应的数据,所以证明了RedisCluster集群的failover能力。

集群数据完整性与可用性

你以为这就结束了?当然没有。
当停止了80节点后,有趣的事情发生了。

自动草稿

集群状态变为了fail,那岂不是整个集群都不可用了。我可是拥有4主4从的集群, 仅down了两台的情况下,就导致整个集群不可用,是不是十分不合理。 那假设我有100台,50主50从,仅down了两台的情况下,剩下98台就都用不了了? 这种重大决策,一定是要交到使用者手中的,根据不同的需求,来选择是否在失去数据完整性的情况下,让集群部分可用。 接着我查阅了大量的资料,最终在RedisCluster的官方文档上找到了答案。

cluster-require-full-coverage <yes/no>: If this is set to yes, as it is by default, the cluster stops accepting writes if some percentage of the key space is not covered by any node. If the option is set to no, the cluster will still serve queries even if only requests about a subset of keys can be processed.

该配置项可以控制当前节点在集群数据不具备完整性的情况下是否继续正常提供服务,默认值为yes。
用刚才的测试举个例子就是:80和85都停了,如果该项设置为no那么集群中剩下的机器可以正常提供服务,反之亦然。

修改了该配置项后,停掉80和85

自动草稿

可以看出,集群的状态并没有变成fail,依然是ok。

由之前的测试内容可以看出,MistRay1和MistRay5的key会路由到80/85的主从之中,所以我们在停掉80/85后, 对插入的其他key进行操作

自动草稿

由上图测试结果可知,客户端是可以对集群内健康的分片进行操作。

总结

RedisCluster高可用有两种模式,当cluster-require-full-coverage设置为yes时,只要集群不具备数据完整性, 那么整个集群直接会进入不可用的状态。当cluster-require-full-coverage设置为no时,即使集群不具备数据完整性, 活着的分片依然可以被操作(读和写)。

0

Jedis连接池会资源泄露吗?

1.前言

Jedis是我们经常使用的Redis Java客户端.在SpringBoot2.X将lettuce作为默认Redis Java客户端之前,Jedis几乎是具备统治地位的.今天我会通过复盘一个压测时遇到的问题来解析Jedis 2.9.1版本一个必现的连接池资源泄露BUG.

2.问题描述

在某次压测中,某服务中产生了这样一条异常日志,期初我们猜测这可能是Jedis连接池负载较高,导致的连接资源紧张的情况.但在持续的压力下,Jedis连接池内的资源竟然大部分都不可用了,最终测试结果以连接池中所有的资源都不可用而告终.

Jedis连接池竟然会资源泄露吗?

3.排查过程

我们马上紧急dump了堆内存,开始分析为什么连接池所有的资源都不可用了,虽然是压测,但压力还没大到把Redis连接池所有资源都繁忙的才对.所以我们一致猜测,应该是某个地方在使用JedisPool中Jedis后没有释放资源导致的.

但在排查了工具类中所有的函数后,发现并没有发现未释放资源的情况.这和我们预想的问题起因不太一致,只能继续排查堆dump,看有没有别的发现.

果不其然,我们发现了堆中Jedis对象都很奇怪,几乎所有Jedis对象里面的socket连接都是closed的状态.socket都close了,当然这个连接就用不了了.但比较诡异的一个点是明明socket已经close了,但表示连接是否损坏的broken字段却是false,意思是并没有损坏.

Jedis连接池竟然会资源泄露吗?

我又注意到了另一个很诡异的问题,明明是从连接池中取出的资源,资源与连接池绑定的映射字段dataSource却是null.难道资源在使用过程中,有什么操作导致资源和池之间解除了绑定关系么?

Jedis连接池竟然会资源泄露吗?

排查到这里基本就可以确定,这个状态不一致的问题就是导致线程池资源耗尽的元凶了,因为连接池认为资源并没有broken,但socket其实已经closed了,连接池也没办法对这些不可用的资源做回收.但想知道为什么会产生这种情况,还是需要去读Jedis的源码.

4.分析源码

从上面的代码可以看到,从JedisPool中获取资源首先要调用getResource()函数.

然后释放资源的时候调用的是Jedisclose()函数.

如果是正常情况下,获取到资源,操作Jedis,最后归还资源到池中,是不会有问题的.但这里有一个非常不明显的线程安全问题.

Jedis连接池竟然会资源泄露吗?

1.线程1在某个资源刚归还到池中并且还没执行到this.DataSource = null

2.同一资源被线程2从池里面获取出来,并将资源与JedisPool绑定

3.线程1执行到this.DataSource = null,将同一资源解绑

4.线程2使用结束后,释放资源,发现dataSource是null认为资源不是从池里取出来的,关闭了socket.

总结

Jedis团队已经在2.10.2版本将该bug修复.

理论上只要并发量够大并且服务启动时间足够长,这个问题几乎是100%复现的.

所以希望看到的小伙伴关注下自己负责的项目中Jedis的版本,如果看到Jedis的close()函数中有this.DataSource = null;这行代码,要尽快把Jedis版本升级到2.10.2及以上版本.

0

Tomcat启动shell脚本

Tomcat启动shell脚本,使用之前修改下面2个参数:

tomcat启动、停止、重启,如下图:

脚本内容如下:

 

0

使用python脚本扫描redis的大key

扫描redis实例中长度大于10240的key打印出来

PS:脚本21行if length > 10240:改成您指定的长度

安装python依赖

  1. pip install redis

扫描出大key

  1. python find_bigkey.py 127.0.0.1 6379 | tee -a bigkey.log
0

RabbitMQ的shell启动脚本

copy过去修改部分参数即可用。

RabbitMQ的shell启动脚本

 

0

关于GPS轨迹压缩的Douglas-Peucker算法简介

前言

最近在做的IOT平台涉及到画轨迹线的业务。谈到轨迹线,设备上报上来的数据量巨大,甚至活跃的设备一天上报来的数据都甚至几十万。前端没法对这个数据去处理进行画线取轨迹图像。所以就有了轨迹压缩。

轨迹压缩算法

轨迹压缩算法分为两大类,分别是无损压缩和有损压缩,无损压缩算法主要包括哈夫曼编码,有损压缩算法又分为批处理方式和在线数据压缩方式,其中批处理方式又包括DP(Douglas-Peucker)算法、TD-TR(Top-Down Time-Ratio)算法和Bellman算法,在线数据压缩方式又包括滑动窗口、开放窗口、基于安全区域的方法等。

本次轨迹压缩决定采用相对简单的DP算法。

DP算法步骤如下:

(1)在轨迹曲线在曲线首尾两点A,B之间连接一条直线AB,该直线为曲线的弦;

(2)遍历曲线上其他所有点,求每个点到直线AB的距离,找到最大距离的点C,最大距离记为dmax;

(3)比较该距离dmax与预先定义的阈值Dmax大小,如果dmax<Dmax,则将该直线AB作为曲线段的近似,曲线段处理完毕;

(4)若dmax>=Dmax,则使C点将曲线AB分为AC和CB两段,并分别对这两段进行(1)~(3)步处理;

(5)当所有曲线都处理完毕时,依次连接各个分割点形成的折线,即为原始曲线的路径。

海伦公式

DP算法中需要求点到直线的距离,该距离指的是垂直欧式距离,即直线AB外的点C到直线AB的距离d,此处A、B、C三点均为经纬度坐标;我们采用三角形面积相等法求距离d,具体方法是:A、B、C三点构成三角形,该三角形的面积有两种求法,分别是普通方法(底x高/2)和海伦公式,海伦公式如下:

假设有一个三角形,边长分别为a、b、c,三角形的面积S可由以下公式求得:GPS轨迹压缩之Douglas-Peucker算法,其中p为半周长:GPS轨迹压缩之Douglas-Peucker算法 。

我们通过海伦公式求得三角形面积,然后就可以求得高的大小,此处高即为距离d。要想用海伦公式,必须求出A、B、C三点两两之间的距离,该距离公式也是一个数学公式。

​ 注意:求出距离后,要加上绝对值,以防止距离为负数。

平均误差

平均误差指的是压缩时忽略的那些点到对应线段的距离之和除以总点数得到的数值。

压缩率

压缩率的计算公式如下: GPS轨迹压缩之Douglas-Peucker算法

具体代码实现

1.为了DP算法压缩之后能匹配到本身数据库查询出的结果记录,(因为结果记录列表的每一条字段是可伸缩的KV对且不固定)准备一个点的实体。

2.DP算法实现类

从入口代码可知dMax是可以传进来的,也就是点到轨迹直线的偏移量阈值是可以设置的。这里我设置默认dMax为30.0测试了一下,4135条数据查询出来有500条,当设置dMax为100时查询出只有289条记录了。具体看业务方需要以多大的压缩限度去压缩。

0

程序员请善待自己的身体

很多程序员会忽略自己的身体健康

程序员作为打工人一族,必须要时刻关注自己的身体健康,因为身体是革命的本钱。但是很多年轻的程序员经常不在乎,认为自己身体倍儿好,经常不吃早饭、熬夜、抽烟。其实这样很伤身体,年轻的时候感觉不到,等到了中年就会发现健康问题不断。

身体健康的重要性

我们的身体有多值钱?其实健康是无价的,如若得了一些重大疾病,那花费都得是几十万甚至是上百万。

即使不从健康的角度,从身体能帮你获得的收益来看一下,它有多重要多值钱。假设小明月收入5000元,一年就是6W元。那如果换成投资,以年化收益10%来计算,那相当于需要60W的本金。也就是说即使是月收入5000元,如果是做投资,你的身体就相当于60W。快赶上三线城市一套小房子了。

那如果月收入是20000元,年收入就是24W,那就相当于投资应有240W本金。也是妥妥的二线城市一套房的价格。

所以,我们的身体其实非常值钱,这就是为什么大家都说,投资自己才是真正的投资。因为你自己的价值是无限的。

但是如果身体出现状况,不能工作了,那不光这些收入没有了,反而因为治病要倒贴很多钱。

如何保持健康

  1. 不要熬夜
  2. 少抽烟,最好别抽
  3. 久坐后要及时舒展筋骨
  4. 适度运动

 

 

 

 

0

什么是所谓的自驱型成长?

最近刚读到一本书,叫做《自驱型成长》,这是一本讲孩子教育的书籍,全书的主题是讲如何才能提高孩子的主动性,让他养成自驱型的成长习惯,就是具有自己内在的驱动力,可以独立安排的自己的事情,具有自己明确的目标和方法。

作者从人的大脑本身结构来讲,什么地方控制着情绪,什么地方控制着理智,还有什么地方控制学习和记忆,以及哪些行为会损害这些区域,以此来引出一个观点,外界的环境会比较直接的影响人的大脑反应,从而引导了一个人的性格的塑造,这说明,从孩子的角度讲,家长的教育方式会直接影响孩子的表现,所以当你质疑,为什么别的孩子都是积极上进,乐观开朗,甚至说学习能力很强,而自己的孩子无论从哪方面来看,似乎都缺了点什么的时候,不妨回忆一下,自己的教育方式和沟通方式是不是有什么问题。

作者展开话题,从很多方面来讲如何做才能够最大的程度的给孩子创造一个良好的生长环境,比如给予适当的压力、无条件的爱孩子、给孩子空间让他做喜欢的事情、建立孩子的信心等,在这些方法里面,我最认同的,就是关于给与孩子“掌控感”的方法。

“掌控感”不等于控制欲,而是一种能够给孩子空间,让他自己表现,通过自己的努力做出一些事情,而且得到别人(家长)的认可的过程,在这个过程中,孩子既能够发挥他自己的创造力,又能够享受到事情做完后带来的成就感,这个过程中,孩子自然而然的就会有自信。

我一直认为,自信这种事情,不是别人教的,当然别人也教不了,而是需要自己给自己创造的,怎么创造?就是通过做一些事情,得到别人认可和赞扬,慢慢的自己在做下一件事的时候就会充满信心。

不吹牛的说,我小时候基本就是属于“别人家的孩子”的这个类别,第一,我学习成绩比较好,第二,我会用很多小东西做一些小发明,比如用马达和泡沫塑料做一个小船、小车,当别人玩买来的遥控车时,我可以自己做一些玩具跟小伙伴玩,第三,我在学校可以帮老师批改作业、试卷,甚至在一些校园活动的时候,我可以当主持人。在别人看来,我这是各方面都很优秀,反过来去批评自己家的孩子。

但是其实不然,我最开始可能只在一两个点比较好,比如成绩说得过去,那么我父母就不会天天催着我写作业,我在做一些小创作小发明的时候,父母也不会觉得我是在“不务正业”,当我做一个东西失败了,没人知道,那我心里压力就会比较小,当我做一个东西成功了,我就会拿出来炫耀,这样会得到很多人的赞扬和肯定,这样的氛围不知不觉给了我极大的信心,这带给我两个好处,一个是我有更多的时间去做自己喜欢的事情,而父母并不会太多干涉,就像书中所说,给孩子空间;二是我有信心去尝试更多的事情,并且莫名的觉得我可以做好,即便最后这件事没有做好,所带来的的挫败感很快也会被别的事情的成就感给覆盖,我还是那个自信并且勇于尝试的人。

所以我从小要面对的是如何才能不自负,而不是如何塑造自信。

这方面,我还是要感谢我父母,他们没有一直给我压力让我去做到班级第一,全校第一,而是说只要考到前五就好,多余的精力可以做点别的有意义的事情,正是这个思路让我从来没有为了学习的事情太多担心;但是不担心不代表我就没有上进心,当我在别的方面都做到比别人好之后,我就发现只有学习这件事似乎没有什么突出点,我一向过于自信的性格,让我觉得我应该在学习上也做得更好一些,于是从我自己的内心的驱动力上来说,我很愿意花很多精力去完成学习任务。

从驱动力来说,我自己本身的驱动力,要大于外界给我的驱动力,这让我做事具有一些主动权,我觉得这也符合书中所说的“自驱型大脑”。

但是这种驱动力在我初中快要毕业时候发生了转变,因为外界给我的压力和期望远远大于自己能够承受的压力了,虽然我在初中毕业那年还是在拼命学习,但是那种压力和紧张在我进入高中后突然就消失了,于是我开始变得放纵,我会逃课打篮球,包夜上网玩游戏,甚至是打架,我也变成了很多人口中的不良少年,如果不是高三那年自己幡然悔悟,开始奋发,恐怕我连上一个本科大学都成问题。

至此我也很清晰地的认知到,如果一个人生活的环境有问题,那么在这个环境中的人的心里和思维方式就不会是一个正常的状态,就很难像具有积极向上、良性抗压、乐观开朗的性格。

这本书加强了我对于成长环境决定一个人性格的认识,当然我也会尽力,在我有了自己的宝宝后,给他一个舒适、健康积极的生长环境。

0

关于Java并发自增计数的探讨

前言
关于并发,一直都有所了解,但并没有真正认真深入的学习过。今天开始探索一下Java并发篇,玩一下多线程。了解了一下关于计数器的多线程下的四种情况。

第一种情况
编写一个类Counter里面有一个成员变量count,写一段简单的i++的代码完成计数的功能,为了暴露多线程下的问题,让每次自增之前睡100ms

开三个线程测试下会出现什么情况

根据代码逻辑,我们很清楚的知道count是一个共享变量,最终结果如果是理想情况不出错的情况下,开三个线程,count最终结果应该是600。然而理想情况一般是不存在的,三个线程避免不了打架,运行情况看下面两张图

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

根据这两张图很明显,中间出现了错误的情况,首先是515线程号12,14输出了两次,而且最终结果是547

这就影响了程序运行的正确性,这就是线程不安全的实例,并发情况出现了。当然了,这种情况解决很简单,应该每个了解过Java多线程的程序员都会知道,一个synchronized关键字就可以解决这样的问题。

synchronized小概念,内部锁,可重入锁。根据JVM计数器实现。
同一个类中相同或不同同步方法,在被调用的时候,调用者是同一个对象,可以重复进入方法体。

第二种情况,我将add()方法代码改为如下:

继续测试结果:

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

如图可见,三个线程规规矩矩的依次执行完,那么这样避免了线程不安全的出现,但是就诞生了另外一个问题,大家想一下,这个是不是跟串行差不了太多了,三个线程依次执行,最终结果虽然没有出错,但是效率也近乎增加了三倍,总耗时应该是三次累计,比起刚才慢了三倍。其实没必要同步整个方法,只需要同步count++就可以了,count在多线程术语中被称为竞态条件。

第三种情况,我再修改下add()方法,这次锁count++,当然输出也要锁到,不然虽然count最终不会出错,但输出有可能会出错:

关于Java并发自增计数的探讨

看到输出结果,count又被三个线程交错增加,输出结果时间几乎同步,效率不会像刚才那种锁方法的低,而且保证了线程安全,用synchronized还是不要无脑锁方法,会影响效率的,但是如果锁实例的时候就要判断好竞态条件,保证加了锁之后最终是一致的,保证线程安全。

此外还有一种方式,第四种情况,利用AtomicInteger原子类对象自增:

测试结果仍然保证了线程安全。

关于Java并发自增计数的探讨

AtomicInteger是Java并发包下面的一个Integer原子操作类。通过命名也可以看得出来,它可以保证原子操作,那么,什么是原子操作?

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。

在这里我的理解,在这个程序里就是,稳定+1,没有其他操作可以妨碍我+1,在程序执行过程中,我这个+1不管是一个步骤还是多个步骤,该线程总要给我+1之后才会执行其他的部分。

还有一种老生常谈的说法是,原子操作(atomic operation)是不需要synchronized的。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。

通过这些解释,可以说明AtomicInteger是可以保证线程安全的。那么再探索一下AtomicInteger的自增方法实现原理吧…

首先点进AtomicInteger的有参构造,注释的意思是:创建一个AtomicInteger(原子整数),给一个初始的value

看下AtomicInteger的成员属性

有一个Unsafe类型的成员变量,上面有一句注释,建立去用Unsafe对象去比较和转换Int为了更新,翻译的有点蹩脚,意思就是利用Unsafe对象去比较转换Int值的变化。其实就是负责与CAS(compare and swap)相关的操作实现。

valueOffset,从命名上来看是一个偏移量,实际上是用来记录内存首地址的偏移量。

value,这个是用来存放int值的。

静态块,是用来给偏移量初始化,在该原子类加载的时候利用字节码对象反射去获取该value在内存上存储的首地址偏移量,意思可以理解为就是该整型变量在内存上存储的位置的一个记录(标识)。

valatile关键字,在这里保证其对其他线程可见。

我调用那个自增方法是incrementAndGet,注释:每个当前value原子的增加1

点到Unsafe类的方法 getAndAddInt(this, valueOffset, 1) 实现

this.getIntVolatile(paramObject, paramLong);方法是利用内存偏移量获取到当前value的值,然后一直利用CAS比较转换,直到CAS比较转换成功。incrementAndGet对应++i,是返回自增之后的值。同样AtomicInteger类中也包含i++,i–,–i之类对应的方法。

说到CAS就会谈到ABA。是CAS引发了ABA问题,CAS也并非完美。就是说,当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。

ABA问题如何解决?

两种优化思路:

AtomicStampedReference 本质是有一个int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式;

AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已;

具体实现见对应源码,本篇就不再解读这两个类的源码了。

0