1. 乐观锁和悲观锁
悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized 关键字 和 Lock 的实现类都是 悲观锁
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
乐观锁的实现方式:①版本号;②CAS;
2. 公平锁和非公平锁
⽣活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平。
源码解读:
为什么要默认是非公平锁?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
使用场景?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
3. 可重入锁
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以 Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
3.1 可重入锁之 synchronized
简单的来说就是:在一个 synchronized 修饰的方法或代码块的内部调用本类的其他 synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
synchronized 可重入实现原理:
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
3.2 可重入锁之 Lock
ReentrantLock 也是可重入锁。
ReentrantLock 的可重入性是通过以下机制实现的:
3.2.1 核心原理
- 状态变量 (state)
ReentrantLock 内部基于 AQS (AbstractQueuedSynchronizer) 实现,利用其state
变量跟踪锁的重入次数:state = 0
:锁未被任何线程持有。state > 0
:锁被某线程持有,数值表示该线程的重入次数。
- 线程所有权检查
AQS 维护一个exclusiveOwnerThread
字段,记录当前持有锁的线程。当线程尝试获取锁时,会检查自身是否为锁的持有者。
3.2.2 获取锁流程(以非公平锁为例)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁未被持有
if (compareAndSetState(0, acquires)) { // CAS 抢锁
setExclusiveOwnerThread(current); // 设置持有线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 当前线程已持有锁
int nextc = c + acquires; // 增加重入次数
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新 state
return true;
}
return false; // 获取失败
}
3.2.3 释放锁流程
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 非持有线程释放锁抛出异常
boolean free = false;
if (c == 0) { // 完全释放锁
free = true;
setExclusiveOwnerThread(null); // 清除持有线程
}
setState(c); // 更新 state
return free; // 返回是否完全释放
}
3.2.4 关键点
- 重入计数:每次重入获取锁时,
state
递增;释放时递减,直到归零后其他线程才能获取。 - 线程绑定:通过检查
exclusiveOwnerThread
确保只有持有线程能操作锁。 - 异常处理:非持有线程尝试释放锁会抛出异常,防止非法操作。
3.2.5 总结
ReentrantLock 通过 AQS 的 state
变量记录重入次数,结合线程所有权检查,实现了可重入性。这使得同一线程可以多次安全地获取和释放锁,避免了自死锁问题,同时保持了锁状态的精确管理。
4. 死锁
略。。。
5. 写锁 读锁
ReentrantReadWriteLock
6. 自旋锁
CAS