在并发编程中,锁是消耗性能的操做,同一时间只能有一个线程进入同步块修改变量的值,好比下面的代码java
synchronized void function(int b){
a = a + b;
}
复制代码
若是不加 synchronized 的话,多线程修改 a 的值就会致使结果不正确,出现线程安全问题。但锁又是要给耗费性能的操做。不管是拿锁,解锁,仍是等待锁,阻塞,都是很是耗费性能的。那么能不能不加锁呢?算法
能够。编程
什么意思呢?咱们看上面的代码,分为几个步骤:缓存
咱们知道,这不是一个原子的操做,多线程上面时候会出问题:当两个线程同时访问 a ,都获得了a 的值,而且通知对a 加 1,而后同时将计算的值赋值给a,这样就会致使 a 的值只增长了1,但实际上咱们想加 2.安全
问题出在哪里?第三步,对 a 赋值操做,若是有一种判断,判断 a 已经别的线程修改,你须要从新计算。好比下面这样:多线程
void function(int b) {
int backup = a;
int c = a + b;
compareAndSwap(a, backup, c);
}
void compareAndSwap(int backup ,int c ){
if (a == backup) {
a = c;
}
}
复制代码
从代码中,咱们看到,咱们备份了 a 的值,而且对 a 进行计算,若是 a 的值和备份的值一致,说明 a 没有被别的线程更改过,这个时候就能够进行修改了。架构
这里有个问题:compareAndSwap 方法有多步操做,不是原子的,而且没有使用锁,如何保证线程安全。其实楼主这里只是伪代码。下面就要好好说说什么是 CAS (compareAndSwap);并发
CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,若是V值和E值不一样,则说明已经有其余线程作两个更新,则当前线程则什么都不作。最后,CAS 返回当前V的真实值。CAS 操做时抱着乐观的态度进行的,它老是认为本身能够成功完成操做。性能
当多个线程同时使用CAS 操做一个变量时,只有一个会胜出,并成功更新,其他均会失败。失败的线程不会挂起,仅是被告知失败,而且容许再次尝试,固然也容许实现的线程放弃操做。基于这样的原理,CAS 操做即便没有锁,也能够发现其余线程对当前线程的干扰。测试
与锁相比,使用CAS会使程序看起来更加复杂一些,但因为其非阻塞的,它对死锁问题天生免疫,而且,线程间的相互影响也很是小。更为重要的是,使用无锁的方式彻底没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以,他要比基于锁的方式拥有更优越的性能。
简单的说,CAS 须要你额外给出一个指望值,也就是你认为这个变量如今应该是什么样子的。若是变量不是你想象的那样,哪说明它已经被别人修改过了。你就须要从新读取,再次尝试修改就行了。
那么这个CAS 是如何实现的呢?也就是说,比较和交换其实是两个操做,如何变成一个原子操做呢?
这样归功于硬件指令集的发展,实际上,咱们可使用同步将这两个操做变成原子的,可是这么作就没有意义了。因此咱们只能靠硬件来完成,硬件保证一个从语义上看起来须要屡次操做的行为只经过一条处理器指令就能完成。这类指令经常使用的有:
其中,前面的3条是20世纪时,大部分处理器已经有了,后面的2条是现代处理器新增的。并且这两条指令的目的和功能是相似的,在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令实现,而在 ARM 和 PowerPC 架构下,则须要使用一对 ldrex/strex 指令来完成 LL/SC 的功能。
CPU 实现原子指令有2种方式:
经过总线锁定来保证原子性。 总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器咋总线上输出此信号时,其余处理器的请求将被阻塞住,那么该处理器能够独占共享内存。可是该方法成本太大。所以有了下面的方式。
经过缓存锁定来保证原子性。 所谓 缓存锁定 是指内存区域若是被缓存在处理器的缓存行中,而且在Lock 操做期间被锁定,那么当他执行锁操做写回到内存时,处理器不在总线上声言 LOCK# 信号,而时修改内部的内存地址,并容许他的缓存一致性机制来保证操做的原子性,由于缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其余处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
注意:有两种状况下处理器不会使用缓存锁定。
java 在 1.5 版本中提供了 java.util.concurrent.atomic 包,该包下全部的类都是原子操做:
如何使用呢?看代码
public static void main(String[] args) throws InterruptedException {
AtomicInteger integer = new AtomicInteger();
System.out.println(integer.get());
Thread[] threads = new Thread[1000];
for (int j = 0; j < 1000; j++) {
threads[j] = new Thread(() ->
integer.incrementAndGet()
);
threads[j].start();
}
for (int j = 0; j < 1000; j++) {
threads[j].join();
}
System.out.println(integer.get());
}
}
复制代码
上面的代码,咱们启动了1000个线程对 AtomicInteger 变量作了自增操做。结果是咱们预期的1000,表示没有发生同步问题。
咱们看看他的内部实现,咱们找到该类的 compareAndSet 方法,也就是比较而且设置。咱们看看该方法实现:
该方法调用了 unsafe 类的 compareAndSwapInt 方法,有几个参数,一个是该变量的内存地址,一个是指望值,一个是更新值,一个是对象自身。彻底符合咱们以前CAS 的定义。那么 ,这个 unsafe 又是什么呢?
该类在 rt.jar 包中,但不在咱们熟悉的 java 包下,而是 sun.misc 包下。而且都是 class 文件,注释都没有,符合他的名字:不安全。
咱们能构造他吗?不能,除非反射。
咱们看看他的源码:
getUnsafe 方法中,会检查调用 getUnsafe 方法的类,若是这个类的 ClassLoader 不为null ,就直接抛出异常,什么状况下会为null呢?当类加载器是 Bootstrap 加载器的时候,Bootstrap 加载器是没有对象的,也就是说,加载这个类极有多是 rt.jar 下的。
而在最新的 Java 9 当中,该类已经被隐藏。由于该类使用了指针。但指针的缺点就是不安全。
CAS 看起来很是的吊,可是,他仍然有缺点,最著名的就是 ABA 问题,假设一个变量 A ,修改成 B以后又修改成 A,CAS 的机制是没法察觉的,但实际上已经被修改过了。若是在基本类型上是没有问题的,可是若是是引用类型呢?这个对象中有多个变量,我怎么知道有没有被改过?聪明的你必定想到了,加个版本号啊。每次修改就检查版本号,若是版本号变了,说明改过,就算你仍是 A,也不行。
在 java.util.concurrent.atomic 包中,就有 AtomicReference 来保证引用的原子性,但楼主以为有点鸡肋,不如使用同步加互斥,可能会更加高效。
今天咱们从各类角度理解了CAS 的原理,该算法特别的重要,从CPU 都特别的设计一条指令来实现可见一斑。而JDK的源码中,处处都 unSafe 的 CAS 算法,能够说,若是没有CAS ,就没有 1.5 的并发容器。好,今天就到这里。
good luck !!!