synchronized 和 ReentrantLock 的实现原理是什么? 它们有什么区别?
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) 来实现的
- 这种实现方式需要在用户态和核心态之间做转换
- 有很大的性能消耗, 这种传统实现锁的方式被称之为重量锁
- 偏向锁
- 自适应自旋锁