欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
乐观锁与悲观锁
synchronized是悲观锁:
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
CAS操作的就是乐观锁(乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类):
每次不加锁而是假设没有冲突而去完成某项操作,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
如果因为冲突失败就重试,直到成功为止。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景
CAS机制(Compare And Swap)
CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程。
并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
Java中CAS操作的执行依赖于Unsafe类的方法,Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
getAndAddInt()通过一个while循环不断的重试更新要设置的值,直到成功为止,底层调用本地方法:
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
CAS机制当中使用了3个基本操作数:
内存地址V(主内存中存放的V值,所有线程共享)
旧的预期值A(线程上次从内存中读取的V值A存放在线程的帧栈中,每个线程私有)
要修改的新值B(需要写入内存中并改写V值的B值)
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,
才会将内存地址V对应的值修改为B(防止多线程并发问题)。
CAS的缺点:
1.自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题(解决:AtomicStampedReference)
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,
但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号:
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp; //版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public V getReference() {
return pair.reference;
}
public int getStamp() {
return pair.stamp;
}
}
基于CAS实现的原子操作基本类型与数组类型
java.util.concurrent.atomic:该包中提供了许多基于CAS实现的原子操作类
AtomicBoolean,AtomicInteger,AtomicLong等
incrementAndGet() :
以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
getAndIncrement():
以原子的方式将实例中的原值加1,返回的是自增前的旧值;
getAndSet(int newValue):
将实例中的值更新为新值,并返回旧值;
addAndGet(int delta) :
以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray(原子更新引用类型数组中的元素)
addAndGet(int i, int delta):
以原子更新的方式将数组中索引为i的元素与输入值相加;
getAndIncrement(int i):
以原子更新的方式将数组中索引为i的元素自增加1;
compareAndSet(int i, int expect, int update):
将数组中索引为i的位置的元素进行更新
原子引用(AtomicReference:提供了引用变量的读写原子性操作)
原理:CAS + Unsafe
赋值操作不是线程安全的。若想不用锁来实现,可以用AtomicReference<V>这个类,实现对象引用的原子更新。
AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS:
比较的是两个对象的地址是否相等(线程在操作value时不会被中断)
private static AtomicReference<User> reference = new AtomicReference<>();
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user); //a,1
System.out.println(reference.get()); //b,2
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 final boolean compareAndSet(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;
}
}
}
Java实现自旋锁(非公平锁)
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
//上锁
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
//解锁
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
优点:
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;
不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。