概述
- 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实
- 悲观锁:也叫独占锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有 锁的线程释放锁
乐观锁用到的机制就是CAS
,compare and swap
在多线程条件下(A,B两个线程),假如i = 2,我们想通过i++的方式让 i 的值变为4,可能执行不成功,因为i++不是原子性操作,在底层不是表面上的i++一步完成,而是分成几步来操作,所以两个线程很可能拿到的值都是2,最终结果可能为3,所以在进行这种操作的时候,一般是 加锁来实现,但加锁会使效率低下,因此,从jdk5
开始,java提供了java.util.concurrent.atomic
包来进行原子性操作
而这里面的原子类的方法都差不多,在不加锁的情况下,都是依靠包装类Unsafe
,进行cas操作
我们以AtomicInteger
为例做介绍,它定义了一个private volatile int value
的变量来存储我们的值,volatile
是线程可见的,其中一个线程修改了对其他线程可见,下面我们来演示:
如何进行原子性操作
public class CASDemo { // CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
//public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get()); atomicInteger.getAndIncrement()
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
如代码所示,我们列举几个常用的方法
1.compareAndSet()
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.compareAndSet(2020, 2021)
如代码所示,该方法有两个参数,我们进入源码看一下:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
根据源码我们可以知道,第一个参数是期望的值,在new
的时候我们传进来的值和该值相比较,如果相等,就更新该原子类
的值为第二个参数update
的值,**那么该过程是如何实现的呢?**我们继续探究源码:在文章开始就说过,Atomic
包的原子性操作依靠的是包装类Unsafe
,我们看一下该类都有什么内容(只截取其中一部分):
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
我们发现该类实现原子操作的方法都加上了native
关键字,并且都是compareSwap***
的形式,我们将这种原子操作称为CAS 操作
,因为java
无法直接操作硬件,只能通过调用本地方法即调用C++
来完成线程相关的操作,我们继续探究刚才的问题:
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
这步是怎样完成的,我们通过源码知道这是本地方法的实现方式,那这些参数有何意义,首先this
当前自身对象,valueOffset
表示该对象在内存中的地址值,expect
代表预期原值,如果当前对象在内存中的值和期望原值相等,就将内存的值改为新值也就是update
2.getAndIncrement()
这个方法和i++的目的一样,使当前的值自增1,具体用法如下:
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.getAndIncrement()
将AtomicInteger
类的值自增1,我们看一下源代码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
...//省略部分代码
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
private volatile int value;
}
我们发现是调用unsafe
类中的getAndAddInt()
方法,而参数valueOffset
是获得当前对象的内存偏移值,我们进入这个方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们可以清楚的发现和我们之前介绍的同样用到了compareAndSwapInt()
方法,这里不赘述,需要注意的是var5 = this.getIntVolatile(var1, var2);
表示从内存地址为var2
处获取对象var1
的值,接下来,如果和期望原值var5
相同,就替换为var5+var4
,var4
就是传下来的参数1,这里通过do... while
的方式,是通过一种自旋锁的方式,如果成功就返回结果,否则重试直到成功为止,有兴趣的同学可以去查一下
ABA问题的引出:
java.util.concurrent.atomic
包提供了许多可以进行原子更新的类,这些类都使用了unsafe
来实现原子操作也就是Compare And Swap(CAS)
,CAS
实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就会导致出现ABA
问题。
- 简单来说就是:有两个线程A和B,假如A线程想将变量i = 1的值变为2,但此时有另一个线程B在A操作之前先对变量i进行操作了,比如将i变为3,然后再变为1,这时候A查看内存地址中的值还是1,就执行更新操作,尽管线程A操作成功,但是不代表这个过程就是没有问题的。
ABA问题的解决:原子引用
针对ABA问题可能带来隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>
也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp
,从而避免ABA问题
下面举个例子:
public class CASDemo {
public static void main(String[] args) {
final AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<Integer>(1,1);
new Thread(()->{
int stamp = atomicReference.getStamp();
System.out.println("A1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(1,2,atomicReference.getStamp(),
atomicReference.getStamp() + 1);
System.out.println("A2=>" + atomicReference.getStamp());
System.out.println(atomicReference.compareAndSet(2, 1, atomicReference.getStamp(),
atomicReference.getStamp() + 1));
System.out.println("A3=>" + atomicReference.getStamp());
},"A").start();
new Thread(()->{
int stamp = atomicReference.getStamp();
System.out.println("B1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("B2=>"+atomicReference.getStamp());
},"B").start();
}
}
看一下输出结果:
A1=>1
B1=>1
A2=>2
true
A3=>3
false
B2=>3
我们先来解释一下代码,这段代码想表达的是利用stamp
来控制版本号,当线程执行某个操作的时候,对应的版本号+1,这里的版本号我们设置的是1,atomicReference.compareAndSet(1,2,atomicReference.getStamp(), atomicReference.getStamp() + 1);
,这里在对原有的值进行覆盖的时候,让原来的版本号相应的+1,在A线程睡眠的是后,B线程也拿到了和A线程一样的版本号1,并且这里故意让B睡眠时间长于A,那么A进行了一系列操作,最终使得版本号变为3,这时候B继续执行,用的还是最初的版本号1,这时候问题来了,atomicReference.compareAndSet(1, 6, stamp, stamp + 1)
这里的stamp是1,但是真正的版本号stamp
已经是3了,那么B线程这句代码就不能执行成功,也就无法对原来的值1进行改变,尽管值1没有发生变化,这里就通过版本戳解决了ABA
问题,从本质上看,版本控制也属于乐观锁的实现方式
总结
对于concurrent
包的实现:
通过本文我们了解到CAS
都是通过内存层面来操作volatile变量,所以Java线程通信大概有以下四种套路:
- A线程写volatile变量,B线程读这个变量
- A线程写volatile变量,B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,B线程读这个volatile变量
对于concurrent包下任意的原子类,他们都是先声明共享变量(volatile修饰),然后配合unsafe类的CAS原子操作
和volatile的可见性
来实现线程同步或通信
参考文章:
乐观锁与悲观锁
Java并发:CAS、ABA问题、ABA问题解决方案
Java CAS 和ABA问题