java高并发:CAS无锁原理及广泛应用

简介: java高并发:CAS无锁原理及广泛应用. CAS的原理; 并且讲述了CAS在jdk、JVM、disruptor、数据库中的应用

前言

在现在的互联网技术领域,用户流量越来越大,系统中并发量越来越大,大公司的日活动辄成百上千万。如何面对如此高的并发是当今互联网技术圈一直在努力的事情。
应对高并发需要在各个技术层面进行合理的设计和技术选型才可以。本文只讲述微观层面是如何应对多线程高并发的,介绍著名的CAS原理以及其广泛应用。

本文中jdk版本使用的是jdk1.7.0_55. 不同版本实现可能稍有差异.

CAS无锁实现原理

为什么要用CAS

在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗是不可以忽略掉的。虽然现在基本都是用线程池来尽可能的降低不断创建线程带来的性能损耗。

对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。那多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术CAS(compare and swap)来鉴别线程冲突,一旦检测到冲突,就充实当前操作指导没有冲突为止。

与锁相比,CAS会使得程序设计比较负责,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,他比基于锁的方式有更优越的性能,所以在目前被广泛应用,我们在程序设计时也可以适当的使用.

不过由于CAS编码确实稍微复杂,而且jdk作者本身也不希望你直接使用unsafe(后面会讲到)来进行代码的编写,所以如果不能深刻理解CAS以及unsafe还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。

CAS原理分析

CAS算法

一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。

  • 额外引申技术点:volatile

上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile进行保证。
volatile实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到,它是通过添加内存屏障实现的。具体实现原理请自行搜索volatile

AtomicInteger

初次接触CAS的人一般都是通过AtomicInteger这个类来了解的,这里讲其原理也借助这个类。

查看一下AtomicInteger的源码:

private volatile int value;

//此处省略一万字代码

/**
 * Atomically sets to the given value and returns the old value.
 *
 * @param newValue the new value
 * @return the previous value
 */
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}


/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return true if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

通过这段代码可知:

  • AtomicInteger中真正存储数据的是value变量,而改变量是被volatile修饰的,保证了线程直接的可见性。还记得Integer中的value吗?Integer中的value是被final修饰的,是不可变对象。
  • getAndSet方法通过一个死循环不断尝试赋值操作。而真正的赋值操作交给了unsafe类来实现。

unsafe

上面可知,Unsafe类是CAS实现的核心。
从名字可知,这个类标记为不安全的,JDK作者不希望用户使用这个类,我们看一下他的构造方法:

public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(var0.getClassLoader() != null) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

如果ClassLoader不是null,直接抛出异常了,我们没办法在应用程序中使用这个类

public static void main(String[] args){
    Unsafe unsafe = Unsafe.getUnsafe();
}

main方法运行结果:

Exception in thread "main" java.lang.SecurityException: Unsafe
    at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
    at com.le.luffi.Tewast.main(Tewast.java:13)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

我们来看一下compareAndSwapInt的方法声明

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

第一个参数是给定的对象,offset是对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),第三个参数是期望值,最后一个是要设置的值。

其实这里Unsafe封装了一些类似于C++中指针的东西,该类中的方法都是native的,而且是原子的操作。原子性是通过CAS原子指令实现的,由处理器保证。

讲到这里相信读者肯定明白CAS是个什么鬼了。

在java领域的广泛应用

jdk中的CAS实现

java.util.concurrent.atomic包

该包下的类都是采用CAS来实现的无锁,读者可以亲自去尝试使用。
image

跳跃表java.util.concurrent.ConcurrentSkipListMap

ConcurrentSkipListMap采用典型的空间换取时间策略,它是一个有序的,支持高并发的Map.

实现原理参见 Java多线程(四)之ConcurrentSkipListMap深入分析
他对节点的操作都是CAS机制实现的

无锁队列java.util.concurrent.ConcurrentLinkedQueue

实现原理参见 聊聊并发(六)ConcurrentLinkedQueue的实现原理分析

并发编程网是个不错的网站

JVM中的CAS

堆中对象的分配

我们都知道java调用new object()会创建一个对象,这个对象会被分配到JVM的堆中。那么这个对象到底是怎么在堆中保存的呢?

首先,new object()执行的时候,这个对象需要多大的空间,其实是已经确定的,因为java中的各种数据类型,占用多大的空间都是固定的(对其原理不清楚的请自行Google)。那么接下来的工作就是在堆中找出那么一块空间用于存放这个对象。
在单线程的情况下,一般有两种分配策略:

  1. 指针碰撞
    这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工作只是将指针像空闲内存一侧移动对象大小的距离即可。
  2. 空闲列表
    这种适用于内存非规整的情况,这种情况下JVM会维护一个内存列表,记录那些内存区域是空闲的,大小是多少哦啊。给对象分配空间的时候去空闲列表里查询到合适的区域然后进行分配即可

但是JVM不可能一直在单线程状态下运行,那样效率太差了。由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:

  1. CAS
    实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。
  2. TLAB
    如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。

高性能内存队列disruptor中的CAS

实现原理参考并发编程网 并发框架Disruptor译文

数据库乐观锁机制

数据中的锁分为两类:悲观锁和乐观锁,锁还有表级锁、行级锁
表级锁例如:
SELECT * FROM table WITH (HOLDLOCK) 其他事务可以读取表,但不能更新删除
SELECT * FROM table WITH (TABLOCKX) 其他事务不能读取表, 更新和删除
行级锁例如:
select * from table_name where id = 1 for update;

悲观锁(Pressimistic Locking)

对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。例如:
select * from table_name where id = ‘xxx’ for update;
这样查询出来的这一行数据就被锁定了,在这个update事务提交之前其他外界是不能修改这条数据的,但是这种处理方式效率比较低,一般不推荐使用。

乐观锁(Optimistic Locking)

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁的实现:

Update Account set field1=? and field2=? and  version = version+1 where version = ?...(another contidition)
相关文章
|
4天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
19 3
|
4天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
23 2
|
27天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
114 6
|
25天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
29 2
|
XML Java 数据库连接
Java高并发秒杀系统【观后总结】(一)
在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番。
277 0
Java高并发秒杀系统【观后总结】(一)
|
存储 SQL 缓存
Java高并发秒杀系统【观后总结】(四)
在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番。
260 0
Java高并发秒杀系统【观后总结】(四)
|
SQL 缓存 NoSQL
Java高并发秒杀系统【观后总结】(三)
在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番。
238 0
Java高并发秒杀系统【观后总结】(三)
|
JSON JavaScript 前端开发
Java高并发秒杀系统【观后总结】(二)
在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番。
218 0
Java高并发秒杀系统【观后总结】(二)
|
前端开发 Java 数据库连接
Java高并发秒杀系统【观后总结】
项目简介 在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番。 记录在该项目中学到了什么玩意.. 该项目源码对应的gitHub地址(由观看其视频的人编写,并非视频源代码):https://github.com/codingXiaxw/seckill 我结合其资料和观看视频的时候整理出从该项目学到了什么... 项目Dao层 日志记录工具: Mybatis之前没注意到的配置属性: 使用jdbc的getGeneratekeys获取自增主键值,这个属性还是挺有用的。
1662 0
|
1天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者