本篇将介绍一些常见的锁策略和原子操作CAS。
目录
一、锁策略
在开发中,不同的应用场景对锁的需求是不一样的 ,如果只有一种锁的话,必然是无法满足需求的,所以聪明的程序员们想出了各种各样的锁策略来设计各种功能各异的锁
乐观锁与悲观锁
乐观锁:这种锁会乐观的认为多个线程之间对某一个数据的操作总不会发生冲突,所以并不需要真正加锁来限制
悲观锁:这种锁则与乐观锁相反,它认为当多个线程多某一个数据进行操作时,总是会发生冲突,所以每次都会真正加锁来限制
Java中的synchronize锁在创建之初是一把乐观锁,当发现锁竞争越来越激烈时,就会转为悲观锁。
乐观锁虽然不会真正进行加锁,但它会通过引入一些机制来检测数据是否发生冲突,以避免线程安全问题,“版本号”就是一个常见的机制
从图中可以发现线程1和线程2都从内存中读取了数据,并修改了数据同时还更新了版本号,线程1先将数据写回了内存,并更新了版本号,但当线程2进行写回操作时,由于此时内存中的版本号和线程2中工作内存的版本号一致,这样就导致同一个版本有两份数据,有此就检测出这整个过程是存在数据冲突,为了避免这个冲突,乐观锁会让线程2的这次操作直接失效,并重新再次尝试执行。
读写锁与互斥锁
互斥锁:当一个线程获得该锁后,其他线程要想获得该锁,就得等锁释放
读写锁:允许多个线程同时对一个共享资源进行读操作,写操作则会互斥
在读写锁中包含了两把锁,一把读锁,一把写锁,读锁允许多个线程同时获得,而写锁同一时刻只能有一个线程获得。在Java中将读写锁封装在了ReetrantReadWriteLock类中,其中ReetrantReadWriteLock.Readlock为读锁,ReetrantReadWriteLock.WriteLock为写锁,这两种锁都有lock和unlock方法来进行加锁和解锁。
synchronize采用的是互斥锁的策略,读写锁适应于那写读多写少的场景
公平锁与非公平锁
公平锁:各线程之间在进行锁竞争时,按照加锁的顺序获得锁
非公平锁:各线程之间在进行加锁时,按照随机的方式获得锁
公平锁通常会在底层维护一个类似队列的数据结构来保存加锁的顺序,并按顺序让线程获得锁,而非公平锁则会直接通过操作系统的随机调度来让线程获得锁
synchronize是一把非公平锁,线程获得锁的顺序是随机的
轻量级锁和重量级锁
锁具有的严格的‘原子性’,可以追溯到cpu上本身就具有的一些原子操作,操作系统通过这些原子操作构建出一个mutex互斥锁,然后jvm基于mutex互斥锁实现了各种功能各异的具有原子性的锁如关键字synchronize,reentrantLock类等。
轻量级锁:创建锁时不直接使用mutex互斥锁,而是先通过一些人为可控制的代码来避免线程冲突,如果实在不能避免,则使用mutex互斥锁
重量级锁:创建锁时直接就使用mutex互斥锁。
轻量级锁由于不会直接创建mutex互斥锁,所以开销较小,前面所说的乐观锁就是一把轻量级锁。
重量级锁由于使用了mutex互斥锁,所以加锁和解锁的开销都是比较大的,前面所说的悲观锁就是一把重量级锁。
挂起等待锁与自旋锁
挂起等待锁:当一个线程在竞争锁失败后,会直接挂起并进入到阻塞等待状态,直到操作系统来唤醒,才会继续参与锁竞争。
自旋锁:当一个线程在竞争锁失败后,会循环的再次去尝试竞争锁,直到成功获取到锁为止。
自旋锁由于会一直尝试去获得锁,所以一旦锁被释放,就能在第一时间加到锁,但如果 自旋的时间太长,就会一直消耗cpu资源,而挂起等待锁虽然不能第一时间获得锁,但它由于处于阻塞状态所以不会消耗cpu资源。所以,锁策略没有好坏之分,每个锁策略都有自己适应的场景,具体使用怎样的锁策略,应该根据需求而定。
可重入锁与不可重入锁
可重入锁:当线程获得锁后,如果再次去获得这把锁,能够成功获得,不会造成死锁。
不可重入锁:当线程获得锁后,如果再次去获得这把锁,会获取失败,形成死锁。
不可重入锁再次获取锁时为什么会形成死锁呢?
获得不可重入锁的线程再第二次对该锁进行加锁时会失败,陷入阻塞等待状态,但这把锁又需要当前线程来解锁,这就导致这把不可重入锁一直无法进行解锁,从而也就形成了死锁。
Java中的synchronize是一把可重入锁,而由操作系统实现的mutex互斥锁则是一把不可重入锁。
二、CAS
CAS是什么?
CAS是由cpu提供的一个原子操作,他有三个参数,oldValue,Value,newValue,其中第一个参数位与主内存,其他两个则位于工作内存,这个操作具体的执行逻辑如下:
- 将oldvalue读入到工作内存并与value进行比较。
- 如果相同则将oldvalue改为newvalue,并返回true,如果不同则返回false。
伪代码如下
public boolean CAS(int oldValue,int value,int newValue){
if (value == oldValue) {
oldValue = newValue;
return true;
} else return false;
}
因为CAS是一个cpu级的一个原子指令,所以CAS是无锁的。
原子类
在JUC包中提供了很多基于CAS实现的原子类
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampReference
下面我们来看一下这些中的主要方法
方法 | 作用 |
addAndGet(int delta) | i+=delta |
getAndDecrement() | i-- |
decrementAndGet() | --i |
getAndIncrement() | i++ |
incrementAndGet() | ++i |
运用CAS
下面我们来运用CAS实现一下AtomicInteger中的incrementAndGet()方法
public int incrementAndGet(){
//保存旧值
int oldValue = value;
//通过cas判断是否与旧值相等
while(!CAS(value,oldValue,oldValue+1)){
//不相等更新旧值,并继续进行cas,直到与旧值相等,并完成数值更新
oldValue = value;
}
return value;
}
通过CAS还可以来实现自旋锁,代码如下
public class SpinLock {
//表示当前哪个线程加锁
static Object ownner = null;
//加锁
public static void lock(){
//判断是否为空,为空表示没有线程加锁,通过cas将ownner改为当前线程
while (!CAS(ownner,null,Thread.currentThread())){
}
}
//解锁
public static void unlock(){
//将ownner制空
ownner = null;
}
}
ABA问题
ABA问题是CAS的一个经典问题,下面我们通过一个案例来了解一下这个问题
有一天doge去银行取钱,卡里有100元,doge想取走这100元,doge数入好金额,并点击取钱按钮后,ATM突然坏了,既没有显示,也没有吐钱,但后台已经扣费了,此时doge感到很疑惑,就在原地等,这时doge的朋友cheems突然又给doge转了100快,过了一会儿,ATM恢复了,doge一看ATM上显示的钱还是200,就又点击了一次取钱,这次ATM正常运转,并且吐钱了,这样就导致扣了200却只取了100块。
为了解决这个问题,我们需要引入一个版本号,每对数据操作一次都会使版本号加一,每次操作数据时都会对版本号来一次CAS,返回true才能进行操作。
前面所提到的原子类AtomicReference就提供了类似于这种版本号控制的功能。