转载自:https://blog.youkuaiyun.com/yangguangdesenlin/article/details/79263335
略作了一些修改。
无锁:无障碍运行。
实现原理: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即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理
cpu并发特性cas,volatile
cas:比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。cas是一种系统原语(所谓原语属于操作系统用于范围,原语由若干指令组成,用于完成一定功能的过程。原语的执行必须是连续的,在执行过程中不允许中断)。
在X86平台上,CPU提供了在指令执行时期对总线加锁的手段。CPU芯片上有一条引线#HLOCKpin,如果汇编语言的程序中在与一条指令前加上前缀“LOCK”,经过汇编后以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上的cpu就暂时不能通过总线访问内存了,保证了这条指令在多核处理器环境中的原子性。
AtomicInteger
初次接触CAS的人一般都是通过AtomicInteger这个类来了解的,这里讲其原理也借助这个类。
AtomicInteger的源码:
private volatile int value;
//此处省略一万字代码
/**
* Atomically setsto the given value and returns the old value.
*
* @param newValuethe new value
* @return theprevious value
*/
public final int getAndSet(int newValue) {
for (;;) {
int current= get();
if(compareAndSet(current, newValue))
returncurrent;
}
}
/**
* Atomically setsthe value to the given updated value
* if the currentvalue {@code ==} the expected value.
*
* @param expectthe expected value
* @param updatethe new value
* @return true ifsuccessful. False return indicates that
* the actual valuewas not equal to the expected value.
*/
public final boolean compareAndSet(int expect, intupdate) {
returnunsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通过这段代码可知
-AutomicInteger中真正存储数据的是value变量,而改变变量的是被volatile修饰的,保证线程直接的可见性。还记的
Integer中的value值吗?Integer中的value是被final修饰的,是不可变对象。
-getAndSet方法通过一个死循环不断尝试赋值操作。而真正的赋值操作交给了unsafe类来实现。
AutomaticInteger的使用
在java语言中,++i和i++并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字,而automicInteger
则通过一种线程安全加减操作接口。
package TestAtomicInteger;
import java.util.concurrent.atomic.AtomicInteger;
class MyThread implements Runnable {
// static int i = 0;
static AtomicInteger ai=new AtomicInteger(0);
public void run() {
for (int m = 0; m < 1000000; m++) {
ai.getAndIncrement();
}
}
};
public class TestAtomicInteger {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(MyThread.ai.get());
}
}
可以发现的结果都是2000000,也就是说AutomaticInteger是线程安全的。
值得一看的。这里,我们来看看AtomicInteger是如何使用非阻塞算法来实现并发控制的。
AtomicInteger 的关键域只有三个:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
这里,unsafe是java提供的获得对象内存地址访问的类,他的作用就是在更新操作时提供“比较并替换”的作用。实际上就是
AutomicInteger中的一个工具。valueOffset是用来记录value本身在内存的编译地址的。方便比较。
value值是用来储存整数的时间变量, 这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value的最新值(
并发环境下,value可能已经被其他线程更新了)
我们自增代码为例,可以看到这个并发控制的核心算法:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
//这里可以拿到value的最新值
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
//使用unsafe的native方法,实现高效的硬件级别CAS
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe
上面可知,unsafe类是cas的核心。
从名字可知,这个类标记为不安全的,jdk作者不希望用户使用这个类,我们看一下他的构造方法。
public static Unsafe getUnsafe() {
Classvar0 = Reflection.getCallerClass();
if(var0.getClassLoader() != null) {
thrownew SecurityException("Unsafe");
} else {
returntheUnsafe;
}
}
如果ClassLoader不是null,直接抛出异常了,我们没办法在应用程序中使用这个类
public static void main(String[] args){
Unsafeunsafe = Unsafe.getUnsafe();
}
main方法运行结构
Exception in thread "main"java.lang.SecurityException: Unsafe
atsun.misc.Unsafe.getUnsafe(Unsafe.java:90)
atcom.le.luffi.Tewast.main(Tewast.java:13)
atsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
atjava.lang.reflect.Method.invoke(Method.java:606)
atcom.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
我们在看一下compareAndSet的方法声明:
public final native booleancompareAndSwapInt(Object var1, long var2, int var4, int var5);
第一个参数是给定的对象,offset是对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快
速定位字段),第三个参数是 期望值,最后一个是要设置的值。
其中unsafe封装了一些类似于c++中指针的东西,该类的方法都是native的,而且是原子的操作。原子性是通过cas指令
实现的,由处理器保证。
unsafe类,不属于java标准,但是很多java的基础类库,包括一些被广泛使用的高性能开发都是基于Unsafe类开发的,比
如netty,cassandra,hadoop,kafka等,unsafe类在提升java运行效率,增强java语言底层操作能力方面起到了很大的作用
unsafe类使java拥有了想c语言的指针一样的操作能力,同时也带来了指针问题。过度使用Unsafe类会使出错的几率变大
,因此java9之后去掉了unsafe类。
unsafe类使用了单例模式,需要一个静态方法getUnsafe()来获取。但是Unsafe类做了限制,如果普通调用的话,它会
抛出SecurityException异常,只有由主类加载器加载的类才能调用这个方法,其源码如下:
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
AtomicReference介绍
atomicReference是作用于对象进行的原子性操作。
在jdk7中AtomicReference源码如下:
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
// 获取Unsafe对象,Unsafe的作用是提供CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// volatile类型
private volatile V value;
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
public final V get() {
return value;
}
public final void set(V newValue) {
value = newValue;
}
public final void lazySet(V newValue) {
unsafe.putOrderedObject(this, valueOffset, newValue);
}
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final V getAndSet(V newValue) {
while (true) {
V x = get();
if (compareAndSet(x, newValue))
return x;
}
}
public String toString() {
return String.valueOf(get());
}
}
说明:
AtomicReference的源码比较简单,他通过“volicate”和“unsafe”提供CAS函数实现“原子操作”
(01)value 是volatile类型。这保证了:当某线程修改value值时,其他线程看到的value值都是最新的value值。
(02)通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,他操作是原子的,
即线程在操作value时不会被中断。
AtomicReference示例
// AtomicReferenceTest.java的源码
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public static void main(String[] args){
// 创建两个Person对象,它们的id分别是101和102。
Person p1 = new Person(101);
Person p2 = new Person(102);
// 新建AtomicReference对象,初始化它的值为p1对象
AtomicReference ar = new AtomicReference(p1);
// 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
ar.compareAndSet(p1, p2);
Person p3 = (Person)ar.get();
System.out.println("p3 is "+p3);
System.out.println("p3.equals(p1)="+p3.equals(p1));
}
}
class Person {
volatile long id;
public Person(long id) {
this.id = id;
}
public String toString() {
return "id:"+id;
运行结果:
p3 is id:102
p3.equals(p1)=false
结果说明:
新建AtomicReference对象ar时,将它初始化为p1。
紧接着,通过CAS函数对它进行设置。如果ar的值为p1的话,则将其设置为p2。
最后,获取ar对应的对象,并打印结果。p3.equals(p1)的结果为false,这是因为Person并没有覆盖equals()方法,而是采用继承自Object.java
的equals()方法;而Object.java中的equals()实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。
AtomicStampedReference
AtomicReference在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。
AtomicStampedReference正是这么做的。他内部不仅维护了对象值,还维护了一个时间戳(实际上他可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,他必须要更新时间戳。当AtomicStampedReference设置对象时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
AtomicStampedReference的几个API在AtomicReference的基础上新填了有关时间戳的信息:
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareAndSet(V expectedReference,V
newReference,int expectedStamp,int newStamp)
//获得当前对象引用
public V getReference()
//获得当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)
使用AtomicStampedReference在修正那个贵宾卡充值的问题的:
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0);
public static void main(String[] args) {
//模拟多个线程同时更新后台数据库,为用户充值
for(int i = 0 ; i < 3 ; i++) {
final int timestamp=money.getStamp();
newThread() {
public void run() {
while(true){
while(true){
Integerm=money.getReference();
if(m<20){
if(money.compareAndSet(m,m+20,timestamp,timestamp+1)){
System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
break ;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
publicvoid run() {
for(int i=0;i<100;i++){
while(true){
int timestamp=money.getStamp();
Integer m=money.getReference();
if(m>10){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元,余额:"+money.getReference());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}
执行上述代码,可以得到以下输出:
余额小于20元,充值成功,余额:39元
大于10元
成功消费10元,余额:29
大于10元
成功消费10元,余额:19
大于10元
成功消费10元,余额:9
没有足够的金额
可以看到,账户只被赠予了一次。
AtomicIntegerArray
当前可使用的原子数组有:AtomicIntegerArray,AtomicLongArray,和AtomicReferenceArray,分别表示整数数组,long型数组和普通的对象数组。
以AtomicIntegerArray为例,展示原子数组的使用方式。
AtomicIntegerArray本质上是对int[]类型的封装。使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:
//获得数组第i个下标的元素
public final int get(int i)
//获得数组的长度
public final int length()
//将数组第i个下标设置为newValue,并返回旧的值
public final int getAndSet(int i, int newValue)
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true
public final boolean compareAndSet(int i, int expect, intupdate)
//将第i个下标的元素加1
public final int getAndIncrement(int i)
//将第i个下标的元素减1
public final int getAndDecrement(int i)
//将第i个下标的元素增加delta(delta可以是负数)
public final int getAndAdd(int i, int delta)
简单的示例,展示AtomicIntegerArray使用:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[]ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
上述代码第2行,申明了一个内含10个元素的数组。第3行定义的线程对数组内10个元素进行累加操作,每个元素各加1000次。第11行,开启10个这样的线程。因此,可以预测,如果线程安全,数组内10个元素的值必然都是10000。反之,如果线程不安全,则部分或者全部数值会小于10000。
程序的输出结果如下:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,10000, 10000]
这说明AtomicIntegerArray确实合理地保证了数组的线程安全性。
AtomicIntegerFieldUpdater
对于atomicIntegerFieldUpdater的使用稍微有一些约束,约束如下:
(1)字段必须是volatile类型的,在线程之间共享 变量是保证立即可见性eg:volatile int value = 3
(2)字段的描述类型(修饰符public/protected/default/peivate)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
(3)只能是实例变量,不能是类变量,也就是说不能加static关键字。
(4)只能是可修改变量,不能是final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
(5)对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdate只能修改int/long类型的字段,不能修改其包装类型(integer/long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
public class TestAtomicIntegerFieldUpdater {
public static void main(String[] args){
TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
tIA.doIt();
}
public AtomicIntegerFieldUpdater<DataDemo> updater(String name){
return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name);
}
public void doIt(){
DataDemo data = new DataDemo();
System.out.println("publicVar = "+updater("publicVar").getAndAdd(data, 2));
}
}
class DataDemo{
public volatile int publicVar=3;
protected volatile int protectedVar=4;
private volatile int privateVar=5;
public volatile static int staticVar = 10;
//public final int finalVar = 11;
public volatile Integer integerVar = 19;
public volatile Long longVar = 18L;
}