常见锁策略
1.乐观锁与悲观锁
二者本质是对数据处理态度的不同,采取不同的策略
乐观锁
乐观锁就是对数据处理态度持乐观态度,认为数据一般不会发生冲突,只有在数据更新的时候才会,于是只有在更新数据的时候才会进行冲突检测。在操作数据的时候默认不上锁,执行更新(就是将寄存器数据写入主存)的时候才会检测是否有其他人修改了数据,修改了则返回错误,否则更新数据。
乐观锁的一种实现机制
- CAS(Compare And Swap)
用一组图来解释:
(1)两个线程分别进行加一操作,线程一线程二都将主存中的值拿到了工作内存中,oldvalue=0
(2)线程1先完成了加一操作,将新值更新到了主存中value=1,线程2还在计算
(3)线程2完成计算后,要更新值了,这时候CAS的Compare操作发现线程之前保存的oldvalue与主存中的value不一样,在计 算的时候被别人修改了,于是就不能进行赋值。然后他会在循环中将主存中的新值value=1读到线程2中,然后重新计算,再对比oldvalue与value是否相同,这次相同了,于是value就被更新为2了。
总结:CAS机制就是在更新数据的时候将主存中的值与之前保存的主存中的值做对比,如果相同就将寄存器中计算的结果swap到主存中,否则重新读数据计算。
Java标准库中java.util.concurrent.atomic
包内的类都是基于这种机制实现的
例如:AtomicInter类,有一个getAndIncrement方法,就是执行自增操作
伪代码:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
但是CAS机制也存在一个问题:ABA问题
比如以下场景:
A银行卡中有100,A要取50块,ATM创建了两个线程完成-50的操作
正常情况:线程1完成减五十操作后,线程2失败
我们知道线程1完成后,线程2会发现银行卡中只有50块了,与之前保存的值:100块不一样了,就会执行失败。
特殊情况:但是有没有一种可能,在线程1完成减五十的操作后,他的朋友突然给他转了50块,这个加五十的操作赶在线程2减五十执行之前完成,那么就会出现线程2发现卡里有100块,和之前保存的值是一样的,然后执行成功,银行卡就又被扣了50块。
再比如:
你有一瓶水放在桌子上,别人把这瓶水喝完了,然后重新倒上去。你再去喝的时候发现水还是跟之前一样,就误以为是刚刚那杯水。如果你知道了真相,那是别人用过了你还会再用嘛?
为了解决上述问题,于是我们又有了一个解决方案:版本号 在比较旧值与数据当前值时,也要比较版本号 真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1. 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
悲观锁
与乐观锁相反,就是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定,比如刚才两个线程加一操作,如果一个线程读取了主存中的数据,要进行加一操作,别的线程就不能读取主存中的数据了,需要等待该线程执行完,释放锁才行。
常用的实现方式就是sychronized,对代码块上锁
悲观锁与乐观锁使用场景与优劣
- 如果并发冲突概率较大的情况下,就是用悲观锁,否则使用乐观锁
- 但是由于悲观锁的锁特性,会使效率降低,影响并发处理的效率
2.读写锁
因为sychronized使用的锁是排他锁,就是同一时刻只允许一个线程访问资源。但是这样会导致效率过低,比如排他锁不允许许多线程同时读数据,但是多线程只是读数据不写数据是不会产生问题的。
所以读写锁就应运而生了,读写锁其实就是把加锁操作细化了,分为读锁和写锁。
-
情况一:线程一加读锁,线程二加读锁
结果:不会产生锁竞争,和没加锁一样 -
情况二:线程一加写锁,线程二加读锁
结果:和普通锁一样产生竞争 -
情况三:线程一加写锁,线程二加写锁
结果:和普通锁一样产生锁竞争
Java内提供的读写锁:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
3.重量级锁与轻量级锁
重量级锁:主要是依赖操作系统提供的互斥锁mutex,由于涉及大量内核用户态切换,成本较高
轻量级锁:尽量不使用mutex,在用户态代码完成,成本较低
4.自旋锁
自旋锁是一个轻量级锁,锁如其名,自旋锁竞争锁失败了会无限循环,直到获取到锁为止
与挂起等待锁不同,挂起等待锁会放弃CPU,使线程进入阻塞状态。
自旋锁优缺点
- 优点:不会放弃CPU,不会进入阻塞状态,一旦锁被释放,会第一时间获取到锁
- 缺点:浪费CPU资源,一直获取不到锁就会一直等待
synchronized中的轻量级锁大概率是通过自旋锁方式实现的
公平锁与非公平锁
简而言之,公平锁就是遵循先来后到原则,谁先尝试获取锁。
非公平锁就是,不遵循先来后到,当锁被释放,谁都有可能获得锁
可重入锁与不可重入锁
先看以下代码:
public class demo1 {
public static void main(String[] args) {
synchronized (demo1.class) {//第一次加锁
synchronized (demo1.class) {//第二次加锁
}
}
}
}
按一般理解就是,第一次加锁成功了,第二次加锁要等第一次释放掉,但是第一次加锁的释放必须要等第二次加锁的代码执行完了才能继续往后走释放掉锁。
这就成了死锁
于是就引入了可重入锁的概念:同一个线程可以对同一个锁加锁多次
他会在内部记录,该锁的拥有者,如果发现当前要加锁的线程和拥有者是同一个线程,就不会等待挂起,而是直接获取到锁
并且内部还会记录加锁次数,记录是第几次加锁,通过计数器控制释放锁。
不可重入锁就和上面相反。
synchronized是可重入锁