synchronized
关键字和ReentrantLock
在不同JDK版本中的性能差异经历了显著的变化。早期,在JDK 1.5及以前的版本中,ReentrantLock
通常提供了更好的性能,主要是因为synchronized
关键字的实现较为简单,没有太多的优化,导致了较多的上下文切换和线程阻塞。
然而,在JDK 1.6中,Java虚拟机(JVM)对synchronized
进行了重大改进,引入了锁的分层机制和适应性自旋锁,这极大地提高了synchronized
的性能。因此,在JDK 1.6及以后的版本中,synchronized
和ReentrantLock
之间的性能差距大大缩小,甚至在某些情况下synchronized
的性能会优于ReentrantLock
。
在JDK 1.8中,进一步的优化使得synchronized
的性能更加接近甚至有时超过ReentrantLock
,特别是在轻量级锁和偏向锁的使用上。
性能选择
在现代JDK版本中(JDK 1.6及更高版本),synchronized
和ReentrantLock
的性能差异并不显著,选择哪个主要取决于具体的使用场景和需求:
-
代码简洁性 :如果你关心代码的简洁性和易读性,
synchronized
可能是一个更好的选择,因为它不需要显式的锁管理,即不需要手动调用lock()
和unlock()
方法。 -
灵活性和控制 :如果你需要更高级的锁控制,如可中断的等待、定时锁尝试、公平锁等,
ReentrantLock
提供了更多的灵活性和控制选项。 -
线程中断 :如果线程需要响应中断,
ReentrantLock
的lockInterruptibly()
方法提供了这种能力,而synchronized
则不能响应中断,除非你使用Thread.sleep()
或Object.wait()
等方法,这些方法会释放锁并可能抛出InterruptedException
。 -
公平锁 :
ReentrantLock
允许你创建公平锁,而synchronized
总是使用非公平锁。
总结
在大多数情况下,你可以根据上述因素来选择,但在具体选择时,还应考虑整个系统的性能瓶颈是否在于锁的竞争。如果锁的竞争不是很激烈,使用哪种锁可能对整体性能影响不大。如果锁成为性能瓶颈,你应该通过基准测试来确定在你的特定环境中哪种锁更优。
最后,无论选择哪种锁,都应该遵循良好的并发编程实践,如尽量减小锁的范围,避免过度使用锁,以及使用局部变量和不可变对象来减少同步的需要。
jdk8中synchronized内部原理
在Java 8中,synchronized
关键字的实现基于Java虚拟机(JVM)中的监视器锁(monitor lock),这是通过对象头(object header)中的Mark Word来实现的。synchronized
关键字有两种基本用法:用于方法和用于代码块。
监视器锁的实现原理
监视器锁是由JVM实现的,它位于每个对象的头部,包含以下信息:
- 对象的hashcode
- 分代年龄
- 锁标志位
- 线程ID
- 锁计数器
当一个线程试图获取一个对象的锁时,它会检查对象的Mark Word中的锁标志位。如果对象未被锁定(即锁标志位表示无锁状态),线程可以尝试获取锁。如果获取成功,Mark Word会被更新,以包含当前线程的ID和锁的类型(偏向锁、轻量级锁或重量级锁)。
锁升级策略
JVM为了提高锁的性能,采用了锁升级的策略,从偏向锁升级到轻量级锁,再到重量级锁,这个过程是不可逆的。锁升级的目的是尽量避免使用重量级锁,以减少线程的挂起和唤醒所带来的开销。
-
偏向锁:当一个线程首次访问一个同步块时,如果该对象没有被其他线程锁定过,JVM会将锁标志位设置为偏向锁,并将Mark Word中的线程ID设置为当前线程ID。如果后续请求锁的线程是同一个线程,可以直接进入同步代码块,无需额外的同步操作。
-
轻量级锁:当有第二个线程试图获取锁时,偏向锁会升级为轻量级锁。轻量级锁使用CAS(Compare and Swap)操作来尝试获取锁。如果CAS操作成功,线程可以继续执行;如果失败,线程会进行自旋,尝试再次获取锁。
-
重量级锁:当轻量级锁无法满足需求,比如自旋次数过多或线程被调度到其他地方执行,JVM会将锁升级为重量级锁。重量级锁会导致线程的阻塞和上下文切换,性能较差。
synchronized关键字的用法
-
用于方法 :当
synchronized
用于方法时,锁的范围是整个方法体。锁的标识符是方法所属的对象实例(对于静态方法则是类的Class对象)。 -
用于代码块 :当
synchronized
用于代码块时,锁的范围仅限于代码块。锁的标识符由括号中的表达式指定,通常是某个对象实例。
总结
在Java 8中,synchronized
关键字通过监视器锁和锁升级策略实现了高效的并发控制。它通过Mark Word来跟踪锁的状态,使用偏向锁、轻量级锁和重量级锁来适应不同的并发需求,从而提高了锁的性能。
自旋
自旋(Spin)是一种计算机科学中的线程同步机制,通常用于多线程环境下。当一个线程试图获取一个已经被其他线程占用的资源(如锁)时,自旋策略会让线程在一个循环中不断地检查资源是否可用,而不是立即将线程置于等待或阻塞状态。如果在自旋期间资源很快变得可用,这种方法可以避免线程上下文切换的开销,从而提高效率。
自旋锁
自旋锁是自旋机制的一种应用,通常用于实现对共享资源的独占访问。当一个线程试图获取一个已经被另一个线程持有的锁时,该线程将进入一个自旋循环,不断地检查锁是否可用。一旦锁变为可用状态,线程就可以立即获取锁并继续执行。
优点
- 减少上下文切换:自旋锁避免了线程阻塞和上下文切换,这在高频率的短时间锁竞争中特别有用,因为上下文切换本身需要消耗一定的时间。
- 快速响应:如果锁很快就能被释放,自旋锁能够迅速响应,使线程能够立即继续执行。
缺点
- CPU消耗:自旋锁在资源不可用时会持续消耗CPU周期,如果资源长时间不可用,这可能导致CPU资源浪费。
- 不适合长期等待:如果线程需要等待很长时间才能获取锁,自旋锁的效率会非常低,此时阻塞线程并进行上下文切换可能是更好的选择。
Java中的自旋
在Java中,自旋锁可以通过ReentrantLock
类结合LockSupport.park()
和LockSupport.unpark()
方法实现,或者使用Atomic
类提供的原子操作来实现。此外,JVM内部也使用自旋机制来优化synchronized
关键字的性能,例如在轻量级锁阶段使用自旋来等待锁的释放。
自适应自旋
自适应自旋是指自旋的时间长度可以根据前一次锁的获取时间来调整。如果前一次锁等待时间较短,则下一次可能会自旋更长的时间;如果前一次等待时间较长,则可能直接放弃自旋,转而使用阻塞机制。Java的JVM实现中包含了自适应自旋的策略,以优化锁的获取效率。
ReentrantLock是怎么实现的
ReentrantLock
是Java并发库java.util.concurrent.locks
中的一个类,它是一个可重入的互斥锁,提供了比synchronized
关键字更强大的锁定机制。ReentrantLock
的设计是基于抽象的同步器AbstractQueuedSynchronizer
(AQS)框架实现的。
AQS框架
AQS框架是一个用于构建锁和其他同步组件的基础框架。它使用了一个内部的FIFO线程等待队列(CLH锁队列)和一个共享的整型状态值state
,通过原子操作修改state
来控制锁的获取与释放。
ReentrantLock实现
ReentrantLock
通过继承自AbstractQueuedSynchronizer
并实现它的模板方法来实现其功能。具体来说,ReentrantLock
实现了AQS
的isHeldExclusively()
方法和tryAcquire()
与tryRelease()
方法。
-
独占模式
ReentrantLock
是一个独占锁,这意味着在任何时刻只有一个线程可以持有锁。当线程尝试获取锁时,ReentrantLock
会调用AQS
的acquire()
方法,这会进一步调用tryAcquire()
方法。在ReentrantLock
的tryAcquire()
实现中,它会检查state
字段,如果state
为0,则没有线程持有锁,可以尝试获取锁。如果获取成功,state
会增加,表示锁被获取。如果state
不为0,表示已经有线程持有锁,调用线程会被加入到AQS的等待队列中,并可能被阻塞。 -
可重入性
ReentrantLock
支持可重入性,即已经持有锁的线程可以再次获取锁而不会引起死锁。这是通过state
字段来实现的,每当同一线程再次获取锁时,state
的值会递增。当线程释放锁时,会调用AQS
的release()
方法,这会调用tryRelease()
方法,其中state
的值会递减。只有当state
的值为0时,锁才真正被完全释放。
Thread thread
- 这个变量在ReentrantLock
的内部类NonfairSync
和FairSync
中被使用,用于存储当前持有锁的线程引用。这样,当一个线程尝试获取锁时,AQS可以检查这个线程是否已经是锁的持有者,如果是,则允许线程再次获取锁,从而实现可重入性。 -
公平性和非公平性
ReentrantLock
提供了公平和非公平两种锁的实现。公平锁会保证线程按照它们请求锁的顺序获取锁,而非公平锁则可能让当前正在运行的线程优先获取锁,即使有其他线程已经在等待队列中。非公平锁在大多数情况下提供了更好的性能,因为它减少了线程的等待时间,但可能会导致某些线程饿死。
源码层面的实现
在源码中,ReentrantLock
的核心逻辑主要体现在以下几个方法中:
newCondition()
: 创建一个新的Condition
对象,用于实现更复杂的线程等待和通知机制。lock()
: 获取锁,如果没有获取到,则会阻塞当前线程直到获取到锁。tryLock()
: 尝试获取锁,如果获取不到锁则立即返回false
。lockInterruptibly()
: 尝试获取锁,如果在等待过程中线程被中断,则抛出InterruptedException
。unlock()
: 释放锁。
总的来说,ReentrantLock
利用了AQS
框架的能力,通过维护state
字段和线程队列,实现了独占、可重入和可选的公平锁行为。
从AbstractQueuedSynchronizer成员变量的角度详解是如何实现可重入锁的
AbstractQueuedSynchronizer
(AQS)是Java并发包java.util.concurrent
中的一个抽象类,它提供了一个框架用于构建依赖于"先进先出"(FIFO)等待队列的阻塞锁和相关的同步器。AQS设计的核心是使用一个volatile整型成员变量state
来表示同步状态,以及一个FIFO线程等待队列来管理线程的等待和唤醒。
AQS的关键成员变量
AQS中有几个关键的成员变量,它们是实现可重入锁的基础:
-
volatile int state
- 这个变量是所有同步状态的基础。在可重入锁中,它的值表示锁的重入次数。当一个线程第一次获取锁时,state
的值被设置为1,之后每次同一线程再次获取锁时,state
的值递增。当线程释放锁时,state
的值递减,直到state
为0,表示锁完全释放。 -
Thread thread
- 这个变量在ReentrantLock
的内部类NonfairSync
和FairSync
中被使用,用于存储当前持有锁的线程引用。这样,当一个线程尝试获取锁时,AQS可以检查这个线程是否已经是锁的持有者,如果是,则允许线程再次获取锁,从而实现可重入性。 -
Node
类型的双向链表 - 这个链表是AQS中用于管理等待线程的队列。每个Node
代表一个等待锁的线程。当一个线程尝试获取锁失败时,它会被插入到队列的尾部,并可能被阻塞直到锁被释放。
实现可重入性的关键方法
-
tryAcquire(int acquires)
- 这个方法用于尝试获取锁。在ReentrantLock
中,它会检查当前线程是否已经持有锁,如果是,则递增state
的值并返回true
。如果不是,它会检查是否有其他线程持有锁,如果没有,则尝试设置state
并返回true
,否则返回false
。 -
tryRelease(int releases)
- 当线程释放锁时,这个方法会被调用。它会递减state
的值。如果state
变为0,这意味着锁被完全释放,任何等待的线程现在都有机会获取锁。
示例:ReentrantLock的实现
在ReentrantLock
中,NonfairSync
和FairSync
类继承自AQS
,并实现了tryAcquire
和tryRelease
方法。在tryAcquire
方法中,如果当前线程已经持有锁,它会递增state
的值,否则会检查是否有其他线程持有锁。如果state
为0且当前线程能够成功获取锁,state
的值将被设置为1。在tryRelease
方法中,如果state
大于0,它会递减state
的值,如果state
变为0,表示锁被完全释放。
总结
AQS通过state
变量和等待队列来管理锁的获取和释放,以及线程的等待和唤醒。ReentrantLock
利用了AQS的这些机制来实现可重入性,即允许一个已经持有锁的线程再次获取锁,同时保持锁的独占性和线程安全。
java
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
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);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
加锁&释放锁
java
// Sync
public void lock() {
sync.lock();
}
// NonfairSync非公平锁
final void lock() {
if (compareAndSetState(0, 1))//直接忽略等待队列,当前线程直接获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//当前线程加入等待队列,自旋
selfInterrupt();
}
// AQS
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)) {//head获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//当前线程挂起
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {//当前线程获取锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//当前线程可重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 先让等待队列的head获取锁,当前线程加入等待队列
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;
}
}
// 释放锁
public void unlock() {
sync.release(1);
}
// AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//AQS
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒线程,继续执行acquireQueued自旋
}
// Sync
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);
return free;
}
jdk8 synchronized锁定代码块,有10个线程被阻塞等待,当锁释放后,jvm会选取其中一个线程唤醒并获取锁,还是唤醒所有线程竞争锁呢? 唤醒一个线程是否仅执行一个线程的上下文切换,唤醒10个线程是否会执行10次上下文切换?
在 JDK8 中,synchronized
锁定代码块时,如果有10个线程被阻塞等待,当锁释放后,JVM 会选取其中一个线程唤醒并获取锁,而不是唤醒所有线程来竞争锁。这种机制有助于减少上下文切换的开销和线程调度的复杂性。
具体来说,JVM 使用了一种基于队列的机制来管理被阻塞的线程。当一个线程释放锁时,JVM 从队列中选取一个等待最久的线程进行唤醒。这个被唤醒的线程将尝试获取锁,如果成功则继续执行它的任务。
关于上下文切换的问题:
-
唤醒一个线程:在这种情况下,JVM 只会执行一次上下文切换,即从当前持有锁的线程切换到被唤醒的线程。
-
唤醒所有线程:如果唤醒所有等待的线程,会引发更多的上下文切换。假设有10个线程等待,当锁释放时,所有线程被唤醒并竞争锁,这将导致多个上下文切换,因为每个线程都需要由操作系统调度并尝试获取锁。最终,只有一个线程能够成功获取锁,其他线程会再次被阻塞,从而造成不必要的上下文切换开销。
因此,JVM 选择唤醒一个线程的策略,旨在提高系统的效率和性能,避免大量不必要的上下文切换和线程调度开销。
对于 Java 中的 synchronized
锁定代码块,JVM 在处理被阻塞线程的机制如下:
-
锁释放后唤醒机制:当一个线程释放锁时,JVM 会唤醒一个被阻塞的线程,而不是所有等待的线程。这种唤醒策略是为了减少上下文切换和线程调度的开销。
-
上下文切换:
- 唤醒一个线程:仅涉及一次上下文切换,即从当前持有锁的线程切换到被唤醒的线程。
- 唤醒所有线程:如果 JVM 唤醒所有等待的线程,会导致多个上下文切换,因为每个线程都需要被操作系统调度并尝试获取锁。这会引发更多的竞争和不必要的上下文切换。
不过,为了更准确地回答你的问题,以下是更详细的解释:
锁的具体实现细节
Java 的 synchronized
锁是依靠对象监视器 (Monitor) 实现的,在 HotSpot JVM 中,具体的锁实现包括偏向锁、轻量级锁和重量级锁。
偏向锁和轻量级锁
- 偏向锁:主要优化单线程重入,减少锁的开销。
- 轻量级锁:线程竞争不激烈时,用自旋等待减少上下文切换。
重量级锁
- 当锁竞争激烈时,会升级为重量级锁。
- 进入重量级锁的线程会被阻塞,并加入等待队列。
JVM 中的线程唤醒机制
当重量级锁被释放时,JVM 使用以下机制处理等待线程:
-
通知机制 :锁释放时,JVM 使用条件变量 (Condition Variables) 的
notify()
或notifyAll()
方法。notify()
: 唤醒等待队列中的一个线程。notifyAll()
: 唤醒所有等待队列中的线程,但大多数情况下,synchronized
块默认使用类似于notify()
的机制,唤醒一个线程。
-
线程调度:
- 唤醒一个线程:减少上下文切换开销,只涉及一次上下文切换。
- 唤醒所有线程:会导致多个上下文切换,每个线程都需要被操作系统调度和尝试获取锁。
因此,当一个线程释放 synchronized
锁时,JVM 通常会选择唤醒一个线程,而不是所有等待的线程。这种策略优化了性能,避免了不必要的上下文切换和资源竞争。
综上所述,我可以确认的是:在 JDK8 中,当 synchronized
锁被释放时,JVM 会唤醒一个被阻塞的线程,而不是所有等待的线程。唤醒一个线程涉及一次上下文切换,而唤醒多个线程会导致多次上下文切换,增加系统开销。