- Java String
- Java Thread
- Java ThreadPool
- Java synchronized 和 ReentrantLock
- Java 深克隆 和 浅克隆
- Java Lock
- Java HashMap
一、synchronized
和 ReentrantLock
实现原理
- 在 JDK-1.5 之前,共享对象的协调机制只有
synchronized
和volatile
。- 在 JDK-1.5 中增加了新的机制
ReentrantLock
。- 该机制的诞生并不是为了替代
synchronized
,而是在synchronized
不适用的情况下,提供一种可以选择的高级功能。
1. synchronized
悲观锁
- synchronized 属于独占式 悲观锁。
- 通过
JVM
隐式实现的,synchronized
只允许同一时刻只有一个线程操作资源。- 在
Java
中每个对象都隐式包含一个 monitor(监视器对象)- 加锁的过程,其实就是竞争
monitor
的过程。
- 当线程进入字节码
monitorenter
指令之后,线程将持有monitor
对象。- 执行
monitorexit
时,释放 monitor 对象。- 当其他线程没有拿到
monitor
对象时,则需要阻塞等待获取该对象。
2. ReentrantLock
- ReentrantLock 是 Lock 的默认实现方式之一。
- 它是基于
AQS
(Abstract Queued Synchronizer,队列同步器)实现的。- 它默认是通过 非公平锁 实现的,在它的内部有一个
state
的状态字段,用于表示锁是否被占用。
- 如果是 0 则表示锁未被占用,此时线程就可以把
state
改为 1,并成功获得锁。- 而其他未获得锁的线程只能去排队等待获取锁资源。
- synchronized 和 ReentrantLock 都提供了锁的功能。
- 具备 互斥性 和 不可见性。
- 在 JDK-1.5 中
synchronized
的性能远远低于ReentrantLock
。- 但在 JDK-1.6 之后,
synchronized
的性能略低于ReentrantLock
。
二、synchronized
和 ReentrantLock
区别
synchronized
是JVM
隐式实现的,而ReentrantLock
是Java
语言提供的 API。ReentrantLock
可设置为公平锁,而synchronized
却不行。ReentrantLock
只能修饰代码块,而synchronized
可以用于修饰方法、修饰代码块等。ReentrantLock
需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而synchronized
无需手动释放锁ReentrantLock
可以知道是否成功获得了锁,而synchronized
却不行。
三、知识扩展
1. ReentrantLock
源码
- 无参的构造函数,创建了一个非公平锁。
- 用户也可以根据第二个构造函数,设置一个
boolean
类型的值,来决定是否使用公平锁来实现线程的调度。
private final Sync sync;
// 可重入锁
public ReentrantLock() {
sync = new NonfairSync();// 非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1.1 公平锁 VS 非公平锁
- 公平锁 的含义是线程需要按照请求的顺序来获得锁。
- 而 非公平锁 则允许"插队"的情况存在。
- 所谓的 插队 指的是,线程在发送请求的同时,该锁的状态恰好变成了可用。
- 那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
- 公平锁 由于有挂起和恢复所以存在一定的开销。
- 因此性能不如 非公平锁。
- 所以
ReentrantLock
和synchronized
默认都是非公平锁的实现方式。
ReentrantLock
是通过lock()
来获取锁,并通过unlock()
释放锁。
2. lock()
获取锁
ReentrantLock
中的lock()
是通过sync.lock()
实现的。- 但
Sync
类中的lock()
是一个抽象方法,需要子类NonfairSync
或FairSync
去实现。
@Test
public void test2() {
Lock lock = new ReentrantLock();try {
lock.lock();// 加锁// ......业务处理} finally {
lock.unlock();// 释放锁}
}
2.1 NonfairSync.lock()
非公平锁源码
/*** 执行锁定。尝试立即进行驳船,并在出现故障时备份到正常状态。*/
final void lock() {
if (compareAndSetState(0, 1))// 将当前线程设置为此锁的持有者setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
2.2 FairSync.lock()
公平锁源码
final void lock() {
acquire(1);
}
- 可以看出 非公平锁 比 公平锁 只是多了一行
compareAndSetState()
方法。
- 该方法是尝试将
state
值由 0 置换为 1。- 如果设置成功的话,则说明当前没有其他线程持有该锁,不用再去排队了,可直接占用该锁。
- 否则,则需要通过
acquire()
方法去排队。
2.3 acquire()
排队获取锁
// AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
2.4 NonfairSync.tryAcquire()
非公平锁尝试获取锁
tryAcquire()
方法尝试获取锁。
- 如果获取锁失败,则把它加入到阻塞队列中。
// NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
2.5 FairSync.tryAcquire()
公平锁尝试获取锁
// FairSync
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);// set state=state+1return true;}return false;
}
- 对于此方法来说,公平锁 比 非公平锁 只多一行代码
!hasQueuedPredecessors()
。
- 它用来查看队列中,是否有比它等待时间更久的线程。
- 如果没有,就尝试一下是否能获取到锁。
- 如果获取成功,则标记为已经被占用。
- 如果获取锁失败,则调用
addWaiter()
方法,把线程包装成Node
对象,同时放入到队列中。- 但
addWaiter
方法并不会尝试获取锁,acquireQueued()
方法才会尝试获取锁。- 如果获取失败,则此节点会被挂起。
2.6 acquireQueued()
源码
/*** AbstractQueuedSynchronizer* 队列中的线程尝试获取锁,失败则会被挂起*/
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 节点出队,等待被 GCfailed = false;// 获取成功return interrupted;}// 判断获取锁失败后是否可以挂起if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程若被中断,返回 trueinterrupted = true;}} finally {
if (failed)cancelAcquire(node);}
}
- 该方法会使用
for(;;)
无限循环的方式来尝试获取锁。
- 若获取失败,则调用
shouldParkAfterFailedAcquire()
方法,尝试挂起当前线程。
2.7 shouldParkAfterFailedAcquire()
源码
*** AbstractOwnableSynchronizer* 判断线程是否可以被挂起*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获得前驱节点的状态int ws = pred.waitStatus;// 前驱节点的状态为 SIGNAL,当前线程可以被挂起(阻塞)if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {
/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {
// 若前驱节点状态为 CANCELLED,那就一直往前找,直到找到一个正常等待的状态为止node.prev = pred = pred.prev;} while (pred.waitStatus > 0);// 并将当前节点排在它后边pred.next = node;} else {
/** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don't park yet. Caller will need to* retry to make sure it cannot acquire before parking.*/// 把前驱节点的状态修改为 SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
- 线程入列被挂起的前提条件是。
- 前驱节点 的状态为 SIGNAL。
- SIGNAL 状态的含义是 后继节点 处于 等待 状态,当前节点释放锁后将会唤醒 后继节点。
- 所以在上面这段代码中,会先判断 前驱节点 的状态。
- 如果为 SIGNAL,则当前线程可以被挂起并返回 true。
- 如果前驱节点 的状态 > 0,则表示 前驱节点 取消了,这时候需要一直往前找,直到找到最近一个正常等待的 前驱节点。
- 然后把它作为自己的 前驱节点;如果前驱节点正常(未取消),则修改 前驱节点 状态为 SIGNAL。
- 到这里整个加锁的流程就已经走完了。
- 最后的情况是,没有拿到锁的线程会在队列中被挂起,直到拥有锁的线程释放锁之后,才会去唤醒其他的线程去获取锁资源。
- 整个运行流程如下图所示:
3. unlock()
释放锁
// ReentrantLock
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer
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()
方法尝试释放锁。- 如果释放成功,则查看头结点的状态是否为 SIGNAL。
- 如果是,则唤醒头结点的下个节点关联的线程。
- 如果释放锁失败,则返回
false
。
3.1 tryRelease()
源码
/*** ReentrantLock* 尝试释放当前线程占有的锁*/
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;
}
- 在
tryRelease()
方法中。
- 会先判断当前的线程是不是占用锁的线程。
- 如果不是的话,则会抛出异常。
- 如果是的话,则先计算锁的状态值
getState() - releases
是否为 0。- 如果为 0,则表示可以正常的释放锁,然后清空独占的线程,最后会更新锁的状态并返回执行结果。
4. JDK-1.6 锁优化
4.1 自适应自旋锁
- JDK-1.5 在升级为 JDK-1.6 时,
HotSpot
虚拟机团队在锁的优化上下了很大功夫。- 比如实现了 自适应式自旋锁、锁升级 等。
- JDK-1.6 引入了自适应式自旋锁,意味着自旋的时间不再是固定的时间了。
- 比如在同一个锁对象上,如果通过自旋等待成功获取了锁。
- 那么虚拟机就会认为,它下一次很有可能也会成功(通过自旋获取到锁)
- 因此允许自旋等待的时间会相对的比较长。
- 而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是 自适应自旋锁 的功能。
4.2 锁升级
- 锁升级 其实就是从 偏向锁 到 轻量级锁 再到 重量级锁 升级的过程。
- 这是 JDK-1.6 提供的优化功能,也称之为 锁膨胀。
4.3 偏向锁
- 偏向锁 是指在无竞争的情况下设置的一种锁状态。
- 偏向锁 的意思是,它会偏向于第一个获取它的线程。
- 当锁对象第一次被获取到之后,会在此对象头中设置标示为 “01”,表示偏向锁的模式,并且在对象头中记录此线程的 ID。
- 这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作。
- 如 Locking、Unlocking 等,直到另一个线程尝试获取此锁的时候,偏向锁模式才会结束。
- 偏向锁 可以提高带有同步,但无竞争的程序性能。
- 但如果在多数锁总会被不同的线程访问时,偏向锁模式就比较多余了。
- 此时可以通过
-XX:-UseBiasedLocking
来 禁用偏向锁 以提高性能。
4.4 轻量锁 和 重量锁
- 轻量锁 是相对于 重量锁 而言的。
- 在 JDK-1.6 之前,
synchronized
是通过操作系统的互斥量(mutex lock
)来实现的。- 这种实现方式,需要在用户态和核心态之间做转换,有很大的性能消耗。
- 这种传统实现锁的方式被称之为 重量锁。
- 而 轻量锁 是通过比较并交换(CAS,Compare and Swap)来实现的。
- 它对比的是线程和对象的
Mark Word
(对象头中的一个区域)
- 如果更新成功,则表示当前线程成功拥有此锁。
- 如果失败,虚拟机会先检查对象的
Mark Word
是否指向当前线程的栈帧。- 如果是,则说明当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用了。
- 当两个以上的线程争抢此锁时,轻量级锁 就膨胀为 重量级锁,这就是 锁升级 的过程,也是 JDK 1.6 锁优化的内容。
四、小结
- 首先是
synchronized
和ReentrantLock
的实现过程。- 然后是
synchronized
和ReentrantLock
的区别。- 最后通过源码的方式,看了
ReentrantLock
加锁和解锁的执行流程。- 接着又看了 JDK-1.6 中的锁优化,包括自适应式自旋锁的实现过程,以及
synchronized
的三种锁状态和锁升级的执行流程。
synchronized
刚开始为偏向锁,随着锁竞争越来越激烈,会升级为轻量级锁和重量级锁。- 如果大多数锁被不同的线程所争抢就不建议使用偏向锁了。