前言
在现在的互联网技术领域,用户流量越来越大,系统中并发量越来越大,大公司的日活动辄成百上千万。如何面对如此高的并发是当今互联网技术圈一直在努力的事情。
应对高并发需要在各个技术层面进行合理的设计和技术选型才可以。本文只讲述微观层面是如何应对多线程高并发的,介绍著名的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来实现的无锁,读者可以亲自去尝试使用。
跳跃表java.util.concurrent.ConcurrentSkipListMap
ConcurrentSkipListMap采用典型的空间换取时间策略,它是一个有序的,支持高并发的Map.
实现原理参见 Java多线程(四)之ConcurrentSkipListMap深入分析
他对节点的操作都是CAS机制实现的
无锁队列java.util.concurrent.ConcurrentLinkedQueue
实现原理参见 聊聊并发(六)ConcurrentLinkedQueue的实现原理分析
并发编程网是个不错的网站
JVM中的CAS
堆中对象的分配
我们都知道java调用new object()
会创建一个对象,这个对象会被分配到JVM的堆中。那么这个对象到底是怎么在堆中保存的呢?
首先,new object()
执行的时候,这个对象需要多大的空间,其实是已经确定的,因为java中的各种数据类型,占用多大的空间都是固定的(对其原理不清楚的请自行Google)。那么接下来的工作就是在堆中找出那么一块空间用于存放这个对象。
在单线程的情况下,一般有两种分配策略:
- 指针碰撞
这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工作只是将指针像空闲内存一侧移动对象大小的距离即可。 - 空闲列表
这种适用于内存非规整的情况,这种情况下JVM会维护一个内存列表,记录那些内存区域是空闲的,大小是多少哦啊。给对象分配空间的时候去空闲列表里查询到合适的区域然后进行分配即可
但是JVM不可能一直在单线程状态下运行,那样效率太差了。由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:
- CAS
实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。 - 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)