本文共 2827 字,大约阅读时间需要 9 分钟。
无锁的原理是使用CAS(Compare And Swap)算法,为什么要用CAS呢?
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗是不可以忽略掉的,虽然现在基本都是用线程池来尽可能的降低不断创建线程带来的性能损耗。
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问是没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。
那么多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术CAS(compare and swap)来鉴别线程冲突,一旦检测到冲突,就重拾当前操作直到没有冲突为止。
与锁相比,CAS使得程序设计比较负责,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有竞争所带来的开销,也没有线程间频繁调度带来的开销,它比基于锁的方式有更优越的性能,所以目前广泛使用。
不过由于CAS编码确实稍微复杂,而且JDK作者本身也不希望你直接使用unsafe来进行代码的编写,所以如果不能深刻理解CAS以及unsafe还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。
CAS算法是由硬件直接支持来保证原子性的,一个CAS方法包含三个参数:
CAS(V,E,N)
- V表示要更新的变量(即内存地址)
- E表示预期的值
- N表示新值
只有当V的值等于E时,才会将V的值修改为N。
如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,然后再次尝试此操作直至修改成功。
基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理
循环CAS( 自旋操作)
在一个 for( ; ; ) 循环里不断的进行CAS操作,直到成功为止volatile
上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile进行保证。volatile实现了JMM的可见性,使得对临界区资源的修改可以马上被其他线程看到,它是通过添加内存屏障实现的CAS的线程安全是通过硬件层面的阻塞来保障实现的
ABA问题:某个线程把值从A改成B,又很快改成A,其他线程获取的时候读取到A,以为没有变更。解决方法是引入版本号1A–>2B–>3A
自旋循环时间很长的话,CPU负荷比较大
对一个变量进行操作可以,但同时操作多个共享变量有点麻烦
JDK中提供了很多无锁原子类的操作:
这些类内部都维护了一个volatile变量,其实就是基于这个变量对于内存可见性的原理进行同步无锁操作。分类如下:AtomicInteger常用API
- public final int get() //取得当前值
- public final void set(int newValue) //立即修改当前值,别的线程马上会看到
- public final void lazySet(int newValue)//不立即修改(最终会修改)当前值,别的线程不会马上看到
- public final int getAndSet(int newValue) //设置新值,并返回旧值
- public final boolean compareAndSet(int expect, int u) //如果当前值为expect,则设置为u
- public final int getAndIncrement() //当前值加1,返回旧值
- public final int getAndDecrement() //当前值减1,返回旧值
- public final int getAndAdd(int delta) //当前值增加delta,返回旧值
- public final int incrementAndGet() //当前值加1,返回新值
- public final int decrementAndGet() //当前值减1,返回新值
- public final int addAndGet(int delta) //当前值增加delta,返回新值
AtomicIntegerArray常用API
- int addAndGet(int i,int delta) //指定数组中第i的数增加delta,返回新值
- boolean compareAndSet(int i,int expect,int update) //比较设置
- 数组通过构造方法传入,类会将数组复制一份,原数组不会发生变化
原子类底层是用Unsafe进行操作的,非公开API,在不同版本JDK中,可能有较大差异,主要API如下:
- public native int getInt(Object o, long offset); //获得给定对象偏移量上的int值
- public native void putInt(Object o, long offset, int x); //设置给定对象偏移量上的int值
- public native long objectFieldOffset(Field f); //获得字段在对象中的偏移量
- public native void putIntVolatile(Object o, long offset, int x); //设置给定对象的int值,使用volatile语义
- public native int getIntVolatile(Object o, long offset); //获得给定对象对象的int值,使用volatile语义
- public native void putOrderedInt(Object o, long offset, int x); //和putIntVolatile()一样,但是它要求被操作字段就是volatile类型的
转载地址:http://wzpxi.baihongyu.com/