Java 中每个对象都隐式包含一个 monitor(监视器) 对象
加锁的过程其实就是竞争 monitor 的过程
当线程进入字节码 monitorenter 指令之后
线程将持有 monitor 对象, 执行 monitorexit 时释放 monitor 对象
当其他线程没有拿到 monitor 对象时, 则需要阻塞等待获取该对象
synchronized
属于独占式悲观锁, 通过JVM隐式实现, 只允许同一时刻只有一个线程操作资源
ReentrantLock
是 Lock 的默认实现方式之一
是基于 AQS(Abstract Queued Synchronizer, 队列同步器) 实现的
默认是通过非公平锁实现的
它的内部有一个 state 的状态字段用于表示锁是否被占用
如果是 0 这表示锁未被占用, 此时线程就可以把 state 改为 1, 并成功获得锁
而其他未获得锁的线程只能去排队等待获取锁资源
1.6版本之后区别
1.5及之前synchronized性能远远低于ReentrantLock, 1.6之后则性能略低于ReentrantLock
1.6及之后区别如下:
synchronized 是 JVM 隐式实现的, 而 ReentrantLock 是 Java 语言提供的 API
ReentrantLock 可设置为公平锁, 而 synchronized 却不行
ReentrantLock 只能修饰代码块, 而 synchronized 可以用于修饰方法, 修饰代码块等
ReentrantLock 需要手动加锁和释放锁, 如果忘记释放锁, 则会造成资源被永久占用, 而 synchronized 无需手动释放锁
ReentrantLock 可以知道是否成功获得了锁, 而 synchronized 却不行
考点
ReentrantLock 的具体实现细节是什么?
构造函数
/**
* 公平锁: 线程需要按照请求的顺序来获得锁
* 非公平锁: 允许"插队"的情况存在(默认实现)
* 插队: 指线程在发送请求的同时该锁的状态恰好变成了可用
* - 那么此线程就可以跳过队列中所有排队的线程直接拥有该锁
*/
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁
}
public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}
NonfairSync lock()
final void lock() {
if(compareAndSetState(0, 1))
// 将当前线程设置为此锁的持有者
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
FairSync lock()
final void lock() {
acquire(1);
}
acquire()
public final void acquire(int arg){
// 尝试获取锁, 如果获取锁失败, 则把它加入到阻塞队列中
if(!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁比非公平锁多了一行代码 !hasQueuedPredecessors()
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // 尝试获取锁
setExclusiveOwnerThread(current); // 获取成功, 标记被抢占
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
acquireQueued(final Node node, int arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 获取锁是否成功的状态标识
try {
boolean interrupted = false; // 线程是否被中断
for (;;) {
final Node p = node.predecessor(); // 获取前一个节点(前驱节点)
if (p == head && tryAcquire(arg)) { // 当前节点为头节点的下一个节点时, 有权尝试获取锁
setHead(node); // 获取成功, 将当前节点设置为 head 节点
p.next = null; // help GC 原 head 节点出队, 等待被 GC
failed = false; // 获取成功
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断获取锁失败后是否可以挂起
parkAndCheckInterrupt())
interrupted = true; // 线程若中断, 返回 true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred,Node node) {
int ws = pred.waitStatus; // 获得前驱节点的状态
if (ws == Node.SIGNAL) // 前驱节点的状态为 SIGNAL, 当前线程可以被挂起 (阻塞)
return true;
if (ws > 0) {
do { // 若前驱节点状态为 CANCELLED, 那就一直往前找, 直到一个正常等待的状态为止
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node; // 并将当前节点排在它后边
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 把前驱节点的状态修改为 SIGNAL
}
return false;
}
unlock()
public void unlock(){
sync.release(1);
}
public final boolean release(int arg){
// 尝试释放锁
if(tryRelease(arg)){
// 释放成功
Node h = head;
if(h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(int releases)
// 尝试释放当前线程占有的锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 释放锁后的状态, 0表示是否锁成功
if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果拥有锁的线程不属于当前线程的话抛出异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 锁被成功释放
free = true;
setExclusiveOwnerThread(null); // 清空独占线程
}
setState(c); // 更新 state 值, 0表示为释放锁成功
return free;
}
JDK1.6 时锁做了哪些优化?
偏向锁
轻量级锁
重量级锁
指在无竞争的情况下设置的一种锁状态
会偏向于第一个获取它的线程
当锁对象第一次被获取到之后, 会在此对象头中设置标识为"01"
表示偏向锁的模式, 并且在对象头中记录此线程的ID
偏向锁可以提高带有同步但无竞争的程序性能
但如果在多数锁总会被不同的现场访问时, 偏向锁模式就比较多余了
此时可以通过 -XX:-UseBiasedLocking 来禁用偏向锁以提高性能
是通过比较并交换 (CAS, Compare and Swap) 来实现的
它对比的是线程和对象的 Mark Word (对象头中的一个区域)
如果更新成功则表示当前线程成功拥有此锁
如果失败, 虚拟机会先检查对象的 Mark Word 是否指向当前线程的栈帧
如果是, 则说明当前线程已经拥有此锁, 否则, 则说明此锁已经被其他线程占用了
当有两个以上的线程争抢此锁时, 轻量级锁就会膨胀为重量级锁
通过操作系统的互斥量(mutex lock) 来实现的
这种实现方式需要在用户态和核心态之间做转换
有很大的性能消耗, 这种传统实现锁的方式被称之为重量锁
自旋的时间不再是固定的时间, 而是通过同一个锁对象自旋等待成功获取锁的几率来调整
自适应自旋锁
锁升级(锁膨胀)