锁机制
synchronized与Lock
Java中有两种加锁的方式:一种是用synchronized关键字,另一种是用Lock接口的实现类。
区别:
悲观锁与乐观锁
锁的一种**宏观分类**方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略
悲观锁(Pessimistic Lock)就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。**悲观锁阻塞事务**
conclusion:Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁
乐观锁(Optimistic Lock),就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作) **乐观锁重试机制** -- CAS算法
乐观锁的基础——CAS
CAS:Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置.
比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)
设置:如果是,将A更新为B,结束。如果不是,则什么都不做
乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
// 模拟CAS
@Slf4j
public class TestCompareAndSwap {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
boolean b = cas.compareAndSet(cas.getValue(), (int) (Math.random() * 101));
log.info("Thread~Name:{},A:{},V:{},trueOrFalse:{}", Thread.currentThread().getName(),
cas.getValue(), (int) (Math.random() * 101), b);
}).start();
}
}
}
class CompareAndSwap {
//内存值 相当于CAS算法中的 V
private int value;
/**
* @return int
* @description: 获取内存值
*/
public synchronized int getValue() {
return value;
}
/**
* @param expectedValue 预估值 相当于CAS算法中的 A
* @param newValue 更新值 相当于CAS算法中的 B
* @return int
* @description: 比较并替换
*/
public synchronized int compareAndSwap(int expectedValue, int newValue) {
//拿到内存中的值
int oldValue = value;
// 如果内存值 = 预估值 就替换(内存值 = 更新值) 否则不做任何操作
if (oldValue == expectedValue) {
this.value = newValue;
}
return oldValue;
}
/**
* @param expectedValue 预估值 相当于CAS算法中的 A
* @param newValue 更新值 相当于CAS算法中的 B
* @return int
* @description: 设置值
*/
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
return expectedValue == compareAndSwap(expectedValue, newValue);
}
}
可重入锁(递归锁、自旋锁)
可重入锁的字面意思是"可以重新进入的锁",即允许同一个线程多次获取同一把锁.比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)
公平锁、非公平锁
**公平锁:**如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平.
**非公平锁:**显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的
对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁.一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁.
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可中断锁
如果线程A持有锁,线程B等待获取该锁.由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁
在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口.
/* Lock接口 */
public interface Lock {
//拿不到锁就一直等,拿到马上返回
void lock();
//拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException
void lockInterruptibly() throws InterruptedException;
//无论拿不拿得到锁,都马上返回.拿到返回true,拿不到返回false
boolean tryLock();
//同上,可以自定义等待的时间
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
Condition newCondition();
}
读写锁、共享锁、互斥锁
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)
// Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
* 读锁
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
* 写锁
* @return the lock used for writing
*/
Lock writeLock();
}
记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改.
读写锁其实做的事情是一样的,但是策略稍有不同.很多情况下,线程知道自己读取数据后,是否是为了更新它.
**那么何不在加锁的时候直接明确这一点呢?**
如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁.我持有写锁的时候别的线程无论读还是写都需要等待
如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)
**虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略.**
因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁.乐观锁特指无锁编程
End 后续再更