在 Java 并发中,我们最初接触的应该就是 synchronized 关键字,但是 synchronized 属于重量级锁,很多时候会引起性能问题, volatile 也是个不错的选择,但是 volatile 不能保证原子性,只能在某些场合下使用。
像 synchronized 这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正好可以进行某项操作,那么发生冲突呢,那我就重试直到成功,乐观锁最常见的就是 CAS 。
① CAS(compare and swap)比较并交换,比较和替换是线程并发算法时用到的一种技术
② CAS 是原子操作,保证并发安全,而不是保证并发同步
③ CAS 是 CPU 的一个指令
④ CAS 是非阻塞的、轻量级的乐观锁
乐观锁,严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现等,所以 CAS 不会保证线程同步。
乐观的认为在数据更新期间没有其它线程影响。
CAS(compare and swap)比较并替换,就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。
举个例子:期望值 E 、内存值 M 、更新值 U ,当 E == M 的时候将 M 更新为 U 。
由于 CAS 是 CPU 指令,我们只能通过 JNI 与操作系统交互,关于 CAS 的方法都在 sun.misc 包下 Unsafe 的类里 java.util.current.atomic 包下的原子类等通过 CAS 来实现原子操作。
public class CasLock { private static CountDownLatch latch = new CountDownLatch(5); // 加法计数器 private static int num = 0; private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { long time = System.currentTimeMillis(); for (int i = 0; i < 5; i++) { new Thread(() -> { for (int x = 0; x < 10000; x++) { num++; //不是原子操作 atomicInteger.getAndIncrement();//调用原子类加1 } latch.countDown(); // 线程执行完成,计数+1 }).start(); } latch.await();//保证所有子线程执行完成 System.out.println("耗时: " + (System.currentTimeMillis() - time) + " 毫秒"); System.out.println("num = " + num); System.out.println("atomicInteger = " + atomicInteger); } }
输出结果:
耗时: 214 毫秒
num = 48450
atomicInteger = 50000
根据结果我们发现,由于多线程异步进行 num++ 操作,导致结果不正确。
为什么 num++ 的记过不正确呢?
比如:两个线程读到 num 的值为 1,然后做加 1 操作,这时候 num 的值是 2,而不是 3 而变量 atomicInteger 的结果却是对的,这就要归功于CAS,下面我们具体看一下原子类。
原子类例如 AtomicInteger 里的方法都很简单,大家看一看都能懂,我们具体看下 getAndIncrement 方法。
//该方法功能是Interger类型加1 public final int getAndIncrement() { //主要看这个getAndAddInt方法 return unsafe.getAndAddInt(this, valueOffset, 1); } //var1 是this指针 //var2 是地址偏移量 //var4 是自增的数值,是自增1还是自增N public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //获取内存值,这是内存值已经是旧的,假设我们称作期望值E var5 = this.getIntVolatile(var1, var2); //compareAndSwapInt方法是重点, //var5是期望值,var5 + var4是要更新的值 //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M //与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作 } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
getAndAddInt 方法的流程:假设有以下情景 ??
① A、B两个线程
② jvm 主内存的值1,A、B工作内存的值为 1(工作内存会拷贝一份主内存的值)
③ 当前期望值为 1,做加 1 操作
④ 此时 var5 = 1,var4 = 1,
- 1. A 线程将 var5 与工作内存值 M 比较,比较 var5 是否等于 1
- 2. 如果相同则将工作内存值修改为 var5 + var4 既修改为 2 并同步到主内存,此时 this 指针里,示例变量 value 的值就是 2 ,结束循环
- 3. 如果不相同则其 B 线程修改了主内存的值,说明 B 线程已经先于 A 线程做了加 1 操作,A 线程没有更新成功需要继续循环,注意此时 var5 更新为新的内存值,假设当前的内存值是 2 ,那么此时 var5 = 2, var5 + var4 = 3 ,重复上述步骤直到成功
下面是compareAndSwapInt本地方法的源码,可以看到使用cmpxchg指令实现CAS,在效率上有不错的表现。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
● 优点:
非阻塞的轻量级的乐观锁,通过 CPS 指令实现,在资源竞争不激烈的情况下性能高,相比 synchronized 重量级锁,synchronized 会进行比较复杂的加锁,解锁和唤醒操作。
● 缺点:
- 1. ABA问题:
CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A ,变成了 B ,又变成了 A ,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
这就是 CAS 的 ABA 问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A 。 目前在 JDK 的 atomic 包里提供了一个类 atomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 2. 循环时间长开销大:
例如 getAndAddInt 方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销
- 3. 只能保证一个共享变量的原子操作:
当对一个共享变量操作时,我们可以采用CAS的方式来保证原子操作。
但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性;
@ - 非原创 - 来源:掘金 - 转载自:zuckerbergJu2.0
原文:https://www.cnblogs.com/Dm920/p/13344329.html