范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

java性能优化实战使用乐观锁和无锁提高代码性能

  Lock 是基于 AQS(AbstractQueuedSynchronizer)实现的,AQS 是用来构建 Lock 或其他同步组件的基础,它使用了一个 int 成员变量来表示state(同步状态),通过内置的 FIFO 队列,来完成资源获取线程的排队。
  synchronized  的方式加锁,会让线程在 BLOCKED 状态和 RUNNABLE 状态之间切换,在操作系统上,就会造成用户态和内核态的频繁切换,效率就比较低。
  与 synchronized 的实现方式不同,  AQS  中很多数据结构的变化,都是依赖 CAS 进行操作的,而  CAS 就是乐观锁的一种实现  。  CAS
  CAS 是 Compare And Swap 的缩写,意思是  比较并替换  。
  如下图,CAS 机制当中使用了 3 个基本操作数:内存地址V、期望值E、要修改的新值N。更新一个变量的时候,只有当变量的预期值E 和内存地址V 的真正值相同时,才会将内存地址V 对应的值修改为 N。
  如果本次修改不成功,怎么办?很多情况下,它将一直重试,直到修改为期望的值。
  拿 AtomicInteger 类来说,相关的代码如下:  public final boolean compareAndSet(int expectedValue, int newValue) {         return U.compareAndSetInt(this, VALUE, expectedValue, newValue); }
  比较和替换是两个动作,CAS 是如何保证这两个操作的原子性呢?
  我们继续向下追踪,发现是   jdk.internal.misc.Unsafe   类实现的,循环重试就是在这里发生的:  @HotSpotIntrinsicCandidate public final int getAndAddInt(Object o, long offset, int delta) {     int v;     do {         v = getIntVolatile(o, offset);     } while (!weakCompareAndSetInt(o, offset, v, v + delta));     return v;  }
  追踪到 JVM 内部,在 linux 机器上参照 os_cpu/linux_x86/atomic_linux_x86.hpp。可以看到,最底层的调用,是汇编语言,而最重要的,就是  cmpxchgl  指令。到这里没法再往下找代码了,  因为 CAS 的原子性实际上是硬件 CPU 直接保证的。  template<> template inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,                                                 T volatile* dest,                                                 T compare_value,                                                 atomic_memory_order /* order */) const {   STATIC_ASSERT(4 == sizeof(T));   __asm__ volatile ("lock cmpxchgl %1,(%3)"                     : "=a" (exchange_value)                     : "r" (exchange_value), "a" (compare_value), "r" (dest)                     : "cc", "memory");   return exchange_value; }
  那 CAS 实现的原子类,性能能提升多少呢?我们开启了 20 个线程,对共享变量进行自增操作。
  从测试结果得知,针对频繁的写操作,原子类的性能是 synchronized 方式的 3 倍。
  CAS 原理,在近几年面试中的考察率越来越高,主要是由于乐观锁在读多写少的互联网场景中,使用频率愈发频繁。
  你可能发现有一些乐观锁的变种,但最基础的思想是一样的,都是基于  比较替换并替换  的基本操作。
  关于 Atomic 类,还有一个小细节,那就是它的主要变量,使用了 volatile 关键字进行修饰。代码如下,你知道它是用来干什么的吗?  private volatile int value;
  答案:使用了 volatile 关键字的变量,每当变量的值有变动的时候,都会将更改立即同步到主内存中;而如果某个线程想要使用这个变量,就先要从主存中刷新到工作内存,这样就确保了变量的可见性。有了这个关键字的修饰,就能保证每次比较的时候,拿到的值总是最新的。  乐观锁
  从上面的描述可以看出,  乐观锁  严格来说,并不是一种锁,它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。
  从这里可以看出乐观锁与悲观锁的一些区别。悲观锁每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。
  乐观锁在检测到冲突的时候,会有多次重试操作,所以之前我们说,乐观锁适合用在读多写少的场景;而在资源冲突比较严重的场景,乐观锁会出现多次失败的情况,造成 CPU 的空转,所以悲观锁在这种场景下,会有更好的性能。
  为什么读多写少的情况,就适合使用乐观锁呢?悲观锁在读多写少的情况下,不也是有很少的冲突吗?
  其实,问题不在于冲突的频繁性,而在于  加锁这个动作  上。  悲观锁需要遵循下面三种模式:一锁、二读、三更新,即使在没有冲突的情况下,执行也会非常慢;  如之前所说,乐观锁本质上不是锁,它只是一个判断逻辑,资源冲突少的情况下,它不会产生任何开销。
  我们上面谈的 CAS 操作,就是一种典型的乐观锁实现方式,我们顺便看一下 CAS 的缺点,也就是乐观锁的一些缺点。  在并发量比较高的情况下,有些线程可能会一直尝试修改某个资源,但由于冲突比较严重,一直更新不成功,这时候,就会给 CPU 带来很大的压力。JDK 1.8 中新增的 LongAdder,通过把原值进行拆分,最后再以 sum 的方式,减少 CAS 操作冲突的概率,性能要比 AtomicLong 高出 10 倍左右。  CAS 操作的对象,只能是单个资源,如果想要保证多个资源的原子性,最好使用synchronized 等经典加锁方式  ABA 问题,意思是指在 CAS 操作时,有其他的线程现将变量的值由 A 变成了 B,然后又改成了 A,当前线程在操作时,发现值仍然是 A,于是进行了交换操作。这种情况在某些场景下可不用过度关注,比如 AtomicInteger,因为没什么影响;但在一些其他操作,比如链表中,会出现问题,必须要避免。可以使用 AtomicStampedReference 给引用标记上一个整型的版本戳,来保证原子性。  乐观锁实现余额更新
  对余额的操作,是交易系统里最常见的操作了。先读出余额的值,进行一番修改之后,再写回这个值。
  对余额的任何更新,都需要进行加锁。因为读取和写入操作并不是原子性的,如果同一时刻发生了多次与余额的操作,就会产生不一致的情况。
  举一个比较明显的例子。你同时发起了一笔消费 80 元和 5 元的请求,经过操作之后,两个支付都成功了,但最后余额却只减了 5 元。相当于花了 5 块钱买了 85 元的东西。请看下面的时序:  请求A:读取余额100 请求B:读取余额100 请求A:花掉5元,临时余额是95 请求B:花掉80元,临时余额是20 请求B:写入余额20成功 请求A:写入余额95成功
  我曾经在线上遇到过一个 P0 级别的 BUG,用户通过构造请求,频繁发起 100 元的提现和 1 分钱的提现,造成了比较严重的后果,你可以自行分析一下这个过程。
  所以,对余额操作加锁,是必须的。   这个过程和多线程的操作是类似的,不过多线程是单机的,而余额的场景是分布式的。
  对于数据库来说,就可以通过加行锁进行解决,拿 MySQL 来说,MyISAM 是不支持行锁的,我们只能使用 InnoDB,典型的 SQL 语句如下:  select * from user where userid={id} for update
  使用 select for update 这么一句简单的 SQL,其实在底层就加了三把锁,非常昂贵。
  默认对主键索引加锁,不过这里直接忽略; 二级索引 userid={id} 的 next key lock(记录+间隙锁); 二级索引 userid={id} 的下一条记录的间隙锁。
  所以,在现实场景中,这种悲观锁都已经不再采用,第一是因为它不够通用,第二是因为它非常昂贵。
  一种比较好的办法,就是使用乐观锁。根据上面我们对于乐观锁的定义,就可以抽象两个概念: 检测冲突的机制 :先查出本次操作的余额E,在更新时判断是否与当前数据库的值相同,如果相同则执行更新动作 重试策略 :有冲突直接失败,或者重试5次后失败
  伪代码如下,可以看到这其实就是 CAS。 # old_balance获取 select balance from  user where userid={id} # 更新动作  update user set balance = balance - 20     where userid={id}      and balance >= 20     and balance = $old_balance
  还有一种 CAS 的变种,就是使用版本号机制。通过在表中加一个额外的字段 version,来代替对余额的判断。这种方式不用去关注具体的业务逻辑,可控制多个变量的更新,可扩展性更强,典型的伪代码如下: version,balance = dao.getBalance(userid) balance = balance - cost dao.exec("     update user      set balance = balance - 20     version = version + 1     where userid=id      and balance >= 20     and version = $old_version ") Redis 分布式锁
  Redis 的分布式锁,是互联网行业经常使用的方案。很多同学知道是使用 setnx 或者带参数的 set 方法来实现的,但 Redis 的分布式锁其实有很多坑。
  在"08 | 案例分析:Redis 如何助力秒杀业务"中,我们演示了一个使用 lua 脚本来实现秒杀场景。但在现实情况中,秒杀业务通常不会这么简单,它需要在查询和用户扣减操作之间,执行一些其他业务。
  比如,进行一些商品校验、订单生成等,这个时候,使用分布式锁,可以实现更灵活地控制,它主要依赖 SETNX 指令或者带参数的 SET 指令。 锁创建:SETNX [KEY] [VALUE] 原子操作,意思是在指定的 KEY 不存在的时候,创建一个并返回 1,否则返回 0。我们通常使用参数更全的 set key value [EX seconds] [PX milliseconds] [NX|XX] 命令,同时对 KEY 设置一个超时时间。 锁查询:GET KEY,通过简单地判断 KEY 是否存在即可 锁删除:DEL KEY,删掉相应的 KEY 即可
  根据原生的语义,我们有下面简单的 lock 和 unlock 方法,lock 方法通过不断的重试,来获取到分布式锁,然后通过删除命令销毁分布式锁。 public void lock(String key, int timeOutSecond) {     for (; ; ) {         boolean exist = redisTemplate.opsForValue().setIfAbsent(key, "", timeOutSecond, TimeUnit.SECONDS);         if (exist) {             break;         }     } } public void unlock(String key) {     redisTemplate.delete(key); }
  这段代码中的问题很多,我们只指出其中一个最严重的问题。在多线程中,执行 unlock方法的,只能是当前的线程,但在上面的实现中,由于超时存在的原因,锁被提前释放了。考虑下面 3 个请求的时序: 请求A:  获取了资源 x 的锁,锁的超时时间为 5 秒 请求A:  由于业务执行时间比较长,业务阻塞等待,超过 5 秒 请求B:  第 6 秒发起请求,结果发现锁 x 已经失效,于是顺利获得锁 请求A:  第 7 秒,请求 A 执行完毕,然后执行锁释放动作 请求C:  请求 C 在锁刚释放的时候发起了请求,结果顺利拿到了锁资源
  此时,请求 B 和请求 C 都成功地获取了锁 x,我们的分布式锁失效了,在执行业务逻辑的时候,就容易发生问题。
  所以,在删除锁的时候,需要判断它的请求方是否正确。首先,获取锁中的当前标识,然后,在删除的时候,判断这个标识是否和解锁请求中的相同。
  可以看到,读取和判断是两个不同的操作,在这两个操作之间同样会有间隙,高并发下会出现执行错乱问题,而稳妥的方案,是使用 lua 脚本把它们封装成原子操作。
  改造后的代码如下: public String lock(String key, int timeOutSecond) {     for (; ; ) {         String stamp = String.valueOf(System.nanoTime());         boolean exist = redisTemplate.opsForValue().setIfAbsent(key, stamp, timeOutSecond, TimeUnit.SECONDS);         if (exist) {             return stamp;         }     } } public void unlock(String key, String stamp) {     redisTemplate.execute(script, Arrays.asList(key), stamp); }
  相应的 lua 脚本如下: local stamp = ARGV[1] local key = KEYS[1] local current = redis.call("GET",key) if stamp == current then     redis.call("DEL",key)     return "OK" end
  可以看到,reids 实现分布式锁,还是有一定难度的。推荐使用 redlock 的 Java 客户端实现 redisson,它是根据 Redis 官方提出的分布式锁管理方法实现的。
  这个锁的算法,处理了分布式锁在多 redis 实例场景下,以及一些异常情况的问题,有更高的容错性。比如,我们前面提到的锁超时问题,在 redisson 会通过看门狗机制对锁进行无限续期,来保证业务的正常运行。
  我们可以看下 redisson 分布式锁的典型使用代码。 String resourceKey = "goodgirl"; RLock lock = redisson.getLock(resourceKey); try {     lock.lock(5, TimeUnit.SECONDS);     //真正的业务     Thread.sleep(100); } catch (Exception ex) {     ex.printStackTrace(); } finally {     if (lock.isLocked()) {         lock.unlock();     } }
  使用 redis 的 monitor 命令,可以看到具体的执行步骤,这个过程还是比较复杂的。
  无锁
  无锁(Lock-Free),指的是在多线程环境下,在访问共享资源的时候,不会阻塞其他线程的执行。
  在 Java 中,最典型的无锁队列实现,就是 ConcurrentLinkedQueue,但它是无界的,不能够指定它的大小。ConcurrentLinkedQueue 使用 CAS 来处理对数据的并发访问,这是无锁算法得以实现的基础。
  CAS 指令不会引起上下文切换和线程调度,是非常轻量级的多线程同步机制。它还把入队、出队等对 head 和 tail 节点的一些原子操作,拆分出更细的步骤,进一步缩小了 CAS 控制的范围。
  ConcurrentLinkedQueue 是一个非阻塞队列,性能很高,但不是很常用。千万不要和阻塞队列 LinkedBlockingQueue(内部基于锁)搞混了。
  Disruptor 是一个无锁、有界的队列框架,它的性能非常高。它使用 RingBuffer、无锁和缓存行填充等技术,追求性能的极致,在极高并发的场景,可以使用它替换传统的 BlockingQueue。
  在一些中间件中经常被使用,比如日志、消息等(Storm 使用它实现进程内部通信机制),但它在业务系统上很少用,除非是类似秒杀的场景。因为它的编程模型比较复杂,而且业务的主要瓶颈主要在于缓慢的 I/O 上,而不是慢在队列上。 小结
  本章中,我们从 CAS 出发,逐步了解了乐观锁的一些概念和使用场景。
  乐观锁 严格来说,并不是一种锁。它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。
  悲观锁 每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。
  乐观锁在读多写少的情况下,之所以比悲观锁快,是因为悲观锁需要进行很多额外的操作,并且乐观锁在没有冲突的情况下,也根本不耗费资源。但乐观锁在冲突比较严重的情况下,由于不断地重试,其性能在大多数情况下,是不如悲观锁的。
  由于乐观锁的这个特性,乐观锁在读多写少的互联网环境中被广泛应用。
  今天我们主要看了在数据库层面的一个乐观锁实现,以及 Redis 分布式锁 的实现,后者在实现的时候,还是有很多细节需要注意的,建议使用 redisson 的 RLock。
  当然,乐观锁有它的使用场景。当冲突非常严重的情况下,会进行大量的无效计算;它也只能保护单一的资源,处理多个资源的情况下就捉襟见肘;它还会有 ABA 问题,使用带版本号的乐观锁变种可以解决这个问题。
  这些经验,我们都可以从 CAS 中进行借鉴。多线程环境和分布式环境有很多相似之处,对于乐观锁来说,我们找到一种检测冲突的机制,就基本上实现了。
  下面留一个问题,大家可以思考下:
  一个接口的写操作,大约会花费 5 分钟左右的时间。它在开始写时,会把数据库里的一个字段值更新为 start,写入完成后,更新为 done。有另外一个用户也想写入一些数据,但需要等待状态为 done。
  于是,开发人员在 WEB 端,使用轮询,每隔 5 秒,查询字段值是否为 done,当查询到正确的值,即可开始进行数据写入。
  开发人员的这个方法,属于乐观锁吗?有哪些潜在问题?应该如何避免?欢迎你在下方留言,一起交流讨论。

初晴的一天从今天起记录我的2023心的力量才是你唯一要追寻的力量。如果此时,你的事业正处于困境当中,你必须意识到,是你的能力认知见识见解让你跌落谷底,而不是运气不好或者太善良。真正带你走出谷盘点2022年娱乐圈的塌房事件元旦小长假过去了,2022年离我们越来越远了,回顾过去的2022年的娱乐圈,小编觉得千言万语汇成一句话塌房年年有,去年特别多。不信,我们来个简单的盘点。黄晓明杨颖官宣离婚2022年膝关节疼痛肿胀,屈伸不利,一剂四神煎消肿祛痛专治膝,请体会大家好,我是中医骨病科阎医生。今天想跟大家聊聊膝关节炎的问题。对于中老年人来说,关节非常容易出问题,关节磨损关节损伤组织退化。在全身关节的诸多关节炎中,膝关节炎又是最常见的一类,我大罗当今足坛,能接替梅西成为第四代球王的仅2人,内马尔不行对于很多人说,梅西自从夺取了卡塔尔世界杯,梅西已经达到了个人巅峰,也是公认的第三代球王,那么问题来了,梅西已经35岁了,谁能完美接替他成为第四代球王呢?对于这个问题,很多人或许有自天神下凡!米切尔见证历史!恭喜天神米切尔成为NBA第七位拿到单场70的球员1米切尔个人生涯单场得分新高(此前米切尔常规赛单场最高为46分)2骑士队史个人单场得分新高(此前是欧文和詹姆斯的57分)3孙雯太精明!暗示不愿接班同流合污,足协掌门成火坑谁敢接陈戌源陈戌源下课在即,谁来接班执掌中国足协?今天,作为热门人选的孙雯罕见发声,似有深意,被外界解读为不想与足协一些人同流合污,如果孙雯不愿接任,中国足协掌门这个烫手山芋,谁敢来接呢?孙雯50守残精,60守残气,70守残血,是什么意思?想长寿,应该怎么做冬日生活打卡季身体最重要的无外乎精神精气精血三个方面,随着身体逐步衰老,每一个阶段都会失去一些,若想要健康长寿,从50岁开始就要注意养寿,守住残精才能安度晚年。也正因此,中医里才会喝酒时不建议吃花生米,这是为什么?医生3种下酒菜也要少碰早在3000多年前的中国就已经出现了酿酒术,随后人们也陆续形成了饮酒庆祝的习惯,如果说在中国历史滚滚向前的过程中,有些文化被抛弃了,而喝酒这个文化应该一直以来都是非常盛行的,甚至有北青达布罗阿德本罗肯定被换王刚邹德海续约国安问题不大直播吧1月3日讯据北京青年报报道,2022赛季结束后,国安面临着阵容调整以及补强的重任。2022年底国安队内众多球员的合同就已经到期,其中王刚邹德海刘国博基本能够与俱乐部续约。国安孤独战士李景亮被抢走的胜利,要一拳拳打回来编者按我们该如何回忆2022年?对于中国拳击和中国搏击,也许就是在遗憾中期待希望。这一年,张伟丽李景亮徐灿们喋血赛场,他们有过身披五星红旗的肆意澎湃,也有过被伤口和裁判剥夺胜利的不多角度复盘卡塔尔世界杯营销战,禹唐体育12日线上举办世界杯营销直播专场为了梳理卡塔尔世界杯背后的营销趋势以及大赛营销玩法,禹唐体育计划在1月12日下午以线上直播的形式举办卡塔尔世界杯营销回顾总结和案例分享直播专场。图片由海信提供卡塔尔世界杯的硝烟早已
3600亿券商资管大动作点蓝字关注,不迷路来源券商中国2023年伊始,已经有两家头部券商递交公募基金牌照申请。近日,广发证券资产管理(广东)有限公司递交公募基金管理人申请材料获得证监会接收。广发证券目前控新春走基层丨值守在青岛地铁的最北端半岛全媒体记者郭振亮青岛地铁目前运营7条线路,运营里程达315公里,稳居全国前十。春节期间,地铁人坚守岗位,保障地铁安全运营,保障市民乘客顺畅出行。新春走基层将讲述他们的坚守故事,流浪地球2影评流浪地球2重生之我是吴京首先强调一下,本人并不是重度科幻片爱好者,星际穿越在我眼里是标准的0分电影,所以对流浪地球2不可能存在夸美贬中的双重标准。故事是流浪地球1的前传,讲述了地球南方多地气温创新低各地启动应急全力保障生产生活春节假期过半,我国南方多地1月25日出现显著降温,上海南京杭州长沙南昌等地的气温创今年入冬以来新低。各地交通电力等部门采取积极应对措施保障生产生活。南方多地气温降至零下出现冰冻天气斯科尔斯腾哈赫不尚虚言!C罗博格巴走是对的,感觉曼联回来了近期,曼联名宿斯科尔斯在OptusSport的节目中讨论了腾哈赫执教曼联的改变,他认为腾哈赫是一个非常务实尚真的主教练,他说他现在有一种感觉曼联要回来了。作为弗格森在英超缔造曼联王WCBA公布全明星大名单,国手潘臻琦和武彤彤落选,有点遗憾今天WCBA公布了全明星大名单,多名国手领衔,阵容基本保持在四川和内蒙古之间,比赛时间在2月4日5日,地点在深圳龙华区文体中心举办。北区教练是木拉提,助理教练是展淑萍,主要球员有杨心有阳光,路有远方人生一世并不长,所以要认真的过好每一天,对热爱的事物不留余力,对热爱的人倾尽真意。人生的悲欢离合,无数重叠的风景,无数重复的日子,藏着我们所有的眷恋。在这喧闹的尘世,我们都需要一个问题探讨不少地方上没钱,同国企大量ampampquot私有化ampampquot关联性到底大不大?近期,据相关媒体数据显示,各个地方城投公司,累积欠款近60余万亿。不少地方上没有充足资金发展,或者没钱解决民生问题等等,都是现实存在。一句话,很多地方没钱,同二十多年前,大量国企被当兵的日子系列(49)之65式白衬衣,大布袜中国开始流行尼龙袜子时较早,据说从抗日战争时期就有了。当时陪都重庆一些权贵商人,从滇缅公路上走私贩私可以挣大钱的物资中就有尼龙(絲)袜子。一些有钱人和地主对这种薄透富有弹力色泽鲜艳霹雳舞新星刘清漪2023年目标是直通巴黎仅在除夕和初一休息了两天,2022年霹雳舞世锦赛亚军刘清漪大年初二(23日)便开始了训练。这位17岁新星告诉记者,她2023年最重要的比赛是世锦赛和亚运会,她要努力拿到直通巴黎奥运女单8强诞生!国乒7朵金花围剿杜凯琹,伊藤模仿者再轰30,稳了1月25日卡塔尔,国际乒联多哈挑战赛继续进行。率先开始的混双项目,国乒三对组合大获全胜,林诗栋和蒯曼击败省队杜凯琹组合袁励岑和张瑞以3个116横扫新加坡一对选手向鹏和钱天一同样直落