锁按照粒度分为悲观锁和乐观锁
(1)悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。synchronized和ReentrantLock都是悲观锁.
(2)乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,CAS就是乐观锁最好的体现.
CAS(Compare And Swap) 比较并交换,在项目中的都是配合循环来实现乐观锁
在java中cas的使用需要Unsafe类,看名字知道是非安全类,不建议使用,所以在平时很少单独的使用cas,都是使用已经包装好的类,比如JUC中的atomic包下的类.
AtomicInteger atomicInteger = new AtomicInteger ();
image.png
atomicInteger.getAndIncrement ()方法源码
public final int getAndAddInt(Object var1, long var2, int var4) {
var1是对象本身
var2 是修改值在内存中的地址
var5 要修改的值
var4 1
int var5;
do {
//从主内存中获取最新的值到工作内存
var5 = this.getIntVolatile(var1, var2);
//修改值,并同步到主内存,如果不相等返回false,继续while循环,如果相等则加一并同步到内存
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Cas的缺点
- Cas和自旋配合使用实现了乐观锁,会出现多个线程同时执行的时候,while多次循环,依然不能保证用户内存和主内存数值一致,这样会增加cpu的使用率,不过是不会出现死循环的现象.
- Cas只是对一个变量操作实现了原子性,不会对一段代码实现原子性.
-
Cas会出现ABA问题
ABA问题
image.png
虽然中间间隔的时间非常短,也会出现ABA问题,那么什么是ABA问题
比如线程1中var5从主内存中获取变量值为 10,代码向下执行到while判断,var5和主内存中的10是否相等,如果相等将10+1,同步主内存和用户内存,
如果
线程1:var5获取变量为10
线程2:一个完整的cas,将变量减一为9
线程3:一个完整的cas,将变量加一为10
线程1:while判断,主内存变量依然为10,与用户内存相等,将10+1,同步到主内存和用户内存
虽然结果和我们得到的一样,但是变量中间经过了两次变化.
那么如何解决ABA问题呢,
AtomicStampedReference
User user = new User ();
User user1 = new User ();
AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference (user,1);
/**
* 第一个参数老值
* 第二个参数新值
* 第三个参数老版本号
* 第四个参数新版本号,注意加一的动作
*/
boolean b = atomicStampedReference.compareAndSet (user, user1, atomicStampedReference.getStamp (), atomicStampedReference.getStamp ()+1);
User reference = atomicStampedReference.getReference ();
System.out.println (reference == user1);
AtomicInteger atomicInteger = new AtomicInteger ();
atomicInteger.getAndIncrement ();
initialRef:是变量user
initialStamp:标签
Pair就是一个静态内部类
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);
}
}
compareAndSet
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//判断是否是一样的user
expectedReference == current.reference &&
//判断版本号是否一致
expectedStamp == current.stamp &&
//判断新的user和新的版本号
((newReference == current.reference &&
newStamp == current.stamp) ||
//如果主内存与当前内存的user和版本号相同,则将新的user和版本号更新主内存
casPair(current, Pair.of(newReference, newStamp)));
}
手写一个cas 这里只是一个示例,因为unsafe类在实际生产中不建议使用
public class CASTest {
static Unsafe unsafe;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//创建unsafe
Class<?> clazz = Unsafe.class;
Field f = clazz.getDeclaredField ("theUnsafe");
f.setAccessible (true);
unsafe = ( Unsafe ) f.get (clazz);
User user = new User ();
user.setName ("zhangsan");
long headOffset = 0;
try {
//偏移量,compareAndSwapObject方法的第二个参数
headOffset = unsafe.objectFieldOffset (User.class.getDeclaredField ("name"));
} catch (NoSuchFieldException e) {
e.printStackTrace ();
}
boolean b = unsafe.compareAndSwapObject (user, headOffset, "zhangsan", "lisi");
System.out.println (user.getName ());
}
}
结果为 lisi
上面的代码共两点
- unsafe是如何创建的,在实际生产中是不建议使用
- User的name为zhangsan
当执行compareAndSwapObject的时候,传入的zhangsan和user对象的name的zhangsan是相等的,所以比较并交换,当前线程的值和主内存的值比较,传入值和user的name值相等,将lisi赋值给user的name属性,就是当前线程和主线程的值都为lisi,而其他线程并不是lisi,需要其他线程在比较并交换的时候先将本线程的值和主内存比较,得到最新的值lisi
如果user的name属性初始为zhangsan1,当比较并交换的时候, compareAndSwapObject传入的值为zhangsan 而user的name属性为zhangsan1不相等,那么当前线程的user在寄存器中的副本的值不变依然为zhangsan1 不会将lisi赋值到user的name属性,并返回b为false.
网友评论