最全的 synchronized 面试题解析

面试之请详细说下synchronized的实现原理以及相关的锁 当轻量级锁获取失败后,就会升级为重量级锁,但是重量级锁之前也介绍了是很耗资源的,JVM开发团队注意到许多程序上,共享数据的二锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程并不值得。... 阅读详情
AI摘要

本文围绕Java中的锁展开,介绍了偏向锁、轻量级锁、重量级锁等多种锁的特点,分析了悲观锁和乐观锁的使用场景,探讨了Synchronized的加锁场景、获取和释放锁的时机等。还对比了synchronized和ReentrantLock的区别,阐述了JVM的锁优化、锁升级流程以及Lock和ReentrantLock的实现原理。

你认识多少种锁?

  • 偏向锁/轻量级锁/重量级锁

  • 可重入锁/非可重入锁

  • 共享锁/独占锁

  • 公平锁/非公平锁

  • 悲观锁/乐观锁

  • 自旋锁/非自旋锁

  • 可中断锁/不可中断锁

锁分别有什么特点?

偏向锁/轻量级锁/重量级锁

这三种锁特指synchronized锁的状态,通过在对象头中的mark word来表明锁的状态。
锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。

偏向锁:自始至终锁都不存在竞争,就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。
轻量级锁:适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级会重量级锁。或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。
重量级锁:重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。

可重入锁/非可重入锁

可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。不可重入锁指的是虽然线程当前持有了这把锁,必须要先释放锁后才能再次尝试获取。
java的juc包中提供 ReentrantLock 就是可重入锁。

共享锁/独占锁

共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

公平锁/非公平锁

公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,遵循先来先得规则。而非公平锁会在一定情况下,忽略掉已经在排队的线程,发生插队现象。synchronized是典型的非公平锁。

悲观锁/乐观锁

悲观锁比较悲观的,预设场景存在多线程在对资源进行竞争的,所以在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁。
乐观锁比较乐观的,预设场景不存多线程在对资源进行竞争的,所以它不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。

自旋锁/非自旋锁

自旋锁:如果线程拿不到锁,不会直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”。
非自旋锁:如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

可中断锁/不可中断锁

不可中断锁:如果线程申请了锁,只能等到拿到锁以后才能进行其他的逻辑处理。synchronized 关键字修饰的锁代表的是不可中断锁。
可中断锁:如果线程申请了锁,不想获取了,可以在中断之后去做其他的事情,不需要一直等到获取到锁才离开。ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法中断之后去做其他的事情。

讲讲java中悲观锁和乐观锁?

悲观锁:synchronized 关键字和 Lock 接口
Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想。
乐观锁:原子类
乐观锁的典型案例就是原子类,例如 AtomicInteger 在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量。
数据库悲观锁和乐观锁:
例如,我们如果在 MySQL 选择 select for update 语句,那就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的。
相反,我们可以利用一个版本 version 字段在数据库中实现乐观锁。在获取及修改数据时都不需要加锁,但是我们在获取完数据并计算完毕,准备更新数据时,会检查版本号和获取数据时的版本号是否一致,如果一致就直接更新,如果不一致,说明计算期间已经有其他线程修改过这个数据了,那我就可以选择重新获取数据,重新计算,然后再次尝试更新数据。
两种锁各自的使用场景:
悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。

Synchronized 各种加锁场景

  1. 作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。
    public synchronized void method() {}

  2. 作用于静态方法,锁住的是类的 Class 对象,Class 对象全局只有一份,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
    public static synchronized void method() {}

  3. 作用于 Lock.class,锁住的是 Lock 的 Class 对象,也是全局只有一个。
    synchronized (Lock.class) {}

  4. 作用于 this,锁住的是对象实例,每一个对象实例有一个锁。
    synchronized (this) {}

  5. 作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
    public static Object monitor = new Object(); synchronized (monitor) {}

synchronized获取和释放monitor锁的时机?

在java中,通常利用 synchronized 关键字来修饰代码块或者修饰一个方法来实现同步。synchronized 的背后正是利用 monitor 锁实现的。所以首先我们来看下获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。synchronized 代码块实际上通过 monitorenter 和 monitorexit 指令实现加锁及释放锁的。

为什么调用Object的wait notify notifyAll方法,需要加synchronized锁?

根本原因,因为这3个方法都会操作monitor锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让我们获取到锁对象。

synchronize 底层维护了几个列表存放被阻塞的线程?

synchronized 底层对应的 JVM 模型为 objectMonitor,使用了3个双向链表来存放被阻塞的线程:_cxq(Contention queue)、_EntryList(EntryList)、_WaitSet(WaitSet)。
当线程获取锁失败进入阻塞后,首先会被加入到 _cxq 链表,_cxq 链表的节点会在某个时刻被进一步转移到 _EntryList 链表。
当持有锁的线程释放锁后,_EntryList 链表头结点的线程会被唤醒,该线程称为 successor(假定继承者),然后该线程会尝试抢占锁。
当我们调用 wait()时,线程会被放入 _WaitSet,直到调用了 notify()/notifyAll() 后,线程才被重新放入 _cxq 或 _EntryList,默认放入 _cxq 链表头部。
objectMonitor 的整体流程如下图:

为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?

因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。这也是 synchronized 为什么是非公平锁的一个原因。

synchronized 是公平锁还是非公平锁?

非公平锁

synchronized 为什么是非公平锁?非公平体现在哪些地方?

1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

  • 先将锁的持有者 owner 属性赋值为 null。

  • 唤醒等待链表中的一个线程(假定继承者)。

  • 在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。

2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒

既然加了 synchronized 锁,那当某个线程调用了 wait 的时候明明还在 synchronized 块里,其他线程怎么进入到 synchronized 里去执行 notify 的?


public class SynchronizedTest {

    private static final Object lock = new Object();

    public static void testWait() throws InterruptedException {
        synchronized (lock) {
            // 阻塞住,被唤醒之前不会输出aa,也就是还没离开synchronized
            lock.wait();
            System.out.println("aa");
        }
    }

    public static void testNotify() throws InterruptedException {
        synchronized (lock) {
            lock.notify();
            System.out.println("bb");
        }
    }
}

当线程进入synchronized 时,需要获取lock锁,但是在调用lock.wait()的时候,此时虽然线程还在synchronized块里,但是其实已经释放掉了lock锁。所以其他线程此时可以获取lock锁进入到synchronized块,从而去执行lock.notify()。

synchronized 和 ReentrantLock 的区别

  1. 底层实现:synchronized是 Java 中的关键字,是 JVM 层面的锁;ReentrantLock 是 JDK 层次的锁实现。

  2. 是否需要手动释放:synchronized 不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;ReentrantLock 在发生异常时,如果没有主动通过 unLock()去释放锁,很可能会造成死锁现象,因此使用 ReentrantLock 时需要在 finally 块中释放锁。

  3. 锁的公平性:synchronized 是非公平锁;ReentrantLock 默认是非公平锁,但是可以通过参数选择公平锁。

  4. 是否可中断:synchronized 是不可被中断的;ReentrantLock 则可以被中断。

  5. 灵活性:使用 synchronized时,等待的线程会一直等待下去,直到获取到锁;ReentrantLock 的使用更加灵活,有立即返回是否成功的,有响应中断、有超时时间等。

  6. 性能上:随着近些年 synchronized 的不断优化,ReentrantLock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。

synchronized 和 Lock 如何选择?

  1. 如果能不用最好既不使用 Lock 也不使用 synchronized。因为在许多情况下你可以使用 java.util.concurrent 包中的机制,它会为你处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁。

  2. 如果 synchronized 关键字适合你的程序, 那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全。

  3. 如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。

JVM 做了哪些锁优化?

  1. 偏向锁(**Biased Locking**):JVM会偏向于第一个获取锁的线程,在这个线程再次获取锁时,会直接获得锁,而不需要进行CAS操作。如果另一个线程尝试获取这个偏向锁,并且偏向锁已经偏向于其他线程,则会进行偏向锁的撤销操作,将偏向锁升级为轻量级锁。偏向锁适用于线程交替执行同步块的场景,大部分锁在整个同步周期内不存在竞争。

  2. 轻量级锁(**Lightweight Locking**):当一个线程尝试获取一个轻量级锁时,如果没有其他线程竞争,它将通过CAS操作成功获取锁。轻量级锁使用CAS操作来实现加锁和解锁,避免了系统调用和线程阻塞的开销。如果存在锁竞争,轻量级锁可能会升级为重量级锁。

  3. 自旋锁(**Spin Lock**):自旋锁是一种忙等待的锁机制,当一个线程尝试获取一个被其他线程持有的锁时,它会进行自旋,即在循环中不断尝试获取锁,而不是立即进入阻塞状态。自旋锁适用于锁持有时间非常短的场景,可以减少线程上下文切换的开销。

  4. 自适应自旋(**Adaptive Spinning**):JDK 1.6引入了自适应自旋锁,自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,并将自旋等待时间延长。

  5. 锁消除(**Lock Elimination**):在编译时,通过逃逸分析等技术,如果JVM检测到一个锁不存在多线程竞争,会对这个对象的锁进行消除,减少不必要的锁操作。

  6. 锁粗化(**Lock Coarsening**):如果一个对象被锁定,并且长时间只有一个线程访问,JVM会将多个连续的锁操作合并成一个锁操作,减少锁的请求、同步与释放带来的性能损耗。

为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?

重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用pthread_mutex_t(互斥锁)来实现。
这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。
而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。

偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?

偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword ,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。
如果确定同步代码块会被多个线程访问或者竞争较大,可以通过 -XX:-UseBiasedLocking参数关闭偏向锁。

自旋发生在哪个阶段?

自旋发生在重量级锁阶段。
在 JDK 8 中,自旋的确是发生在重量级锁阶段,而不是轻量级锁阶段。之前主流的说法大多停留在对早期 JVM 优化的解释中,但从源码角度看,自旋的确是在重量级锁中进行的。轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。

轻量级锁为什么不自旋?

在 JDK 8 中,轻量级锁并没有自旋,而是直接升级为重量级锁。这背后的原因主要与锁的竞争和开销控制有关。

  • 轻量级锁的设计是为了优化低竞争环境下的线程同步。当线程通过 CAS 操作尝试获取轻量级锁失败时,JVM 假定竞争较为激烈,所以直接升级为重量级锁,而不是在这个阶段进行自旋。

  • 轻量级锁阶段的目标是通过最小的开销(CAS 操作)来解决无竞争或低竞争的同步需求。一旦检测到竞争,它立即膨胀为重量级锁,因为轻量级锁的机制并不适合处理高竞争场景。进入重量级锁后,再通过自旋优化锁的获取。

为什么要设计自旋操作?

因为重量级锁的挂起开销太大。在重量级锁阶段,线程已经进入了一个竞争相对激烈的环境,然而锁的持有时间并不总是很长。为了避免在锁持有时间很短的情况下频繁阻塞、唤醒,JVM 引入了自旋锁机制。

自适应自旋是如何体现自适应的?

自适应自旋锁有自旋次数限制,范围在:1000~5000。
如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。
所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。
相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。

synchronized 锁能降级吗?

答案是可以的。
具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。
当锁降级时,主要进行了以下操作:
1)恢复锁对象的 markword 对象头;
2)重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。

synchronized 锁升级流程?

synchronized 的底层实现

synchronized 的底层实现主要区分:方法和代码块。
synchronized 修饰代码块时,编译后会生成  monitorenter 和 monitorexit 指令,分别对应进入同步块和退出同步块。可以看到有两个 monitorexit,这是因为编译时 JVM 为代码块添加了隐式的 try-finally,在 finally 中进行了锁释放,这也是为什么 synchronized 不需要手动释放锁的原因。
synchronized 修饰方法时,编译后会生成 ACC_SYNCHRONIZED 标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了则会先尝试获得锁。
两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

Mark Word?

在HotSpot中,对象在堆内存储布局可以分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

对象头(Header)

主要包含两类信息:Mark Word和 类型指针

Mark Word 记录了对象的运行时数据,例如:HashCode、GC分代年龄、偏向标记、锁标记、偏向的线程ID、偏向纪元(epoch)等,32 位的 markword 如下图所示。

类型指针,指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象时哪个类的实例。如果对象是数组,则需要有一个用于记录数组长度的数据。

实例数据(Instance Data)

对象存储的真正有效信息,即我们在代码里定义的各种类型的字段内容。

对齐填充(Padding)

Hotspot 要求对象的大小必须是8字节的整数倍,因此,如果实例数据不是8字节的整数倍时,需要通过该字段进行填充。

什么是匿名偏向?

所谓的匿名偏向是指该锁从未被获取过,也就是第一次偏向,此时的特点是锁对象 markword 的线程 ID 为0。
当第一个线程获取偏向锁后,线程ID会从0修改为该线程的 ID,之后该线程 ID 就不会为0了,因为释放偏向锁不会修改线程 ID。
这也是为什么说偏向锁适用于:只有一个线程获取锁的场景。

偏向锁模式下 hashCode 存放在哪里?

偏向锁状态下是没有地方存放 hashCode 的。
因此,当一个对象已经计算过 hashCode 之后,就再也无法进入偏向锁状态了。
如果一个对象当前正处于偏向锁状态,收到需要计算其 hashCode 的请求时(Object::hashCode()或者System::identityHashCode(Object)方法的调用),它的偏向锁状态就会立即被撤销。

偏向锁流程?

首先,在开启偏向锁的时候,对象创建后,其偏向锁标记位为1。如果没开启偏向锁,对象创建后,其偏向锁标记位为0。

加锁流程:

1)从当前线程的栈帧中寻找一个空闲的Lock Record,将 obj 属性指向当前锁对象。
2)获取偏向锁时,会先进行各种判断,如加锁流程图所示,最终只有两种场景能尝试获取锁:匿名偏向、批量重偏向。
3)使用 CAS 尝试将自己的线程 ID 填充到锁对象 markword 里,修改成功则获取到锁。
4)如果不是步骤2的两种场景,或者 CAS 修改失败,则会撤销偏向锁,并升级为轻量级锁。
5)如果线程成功获取偏向锁,之后每次进入该同步块时,只需要简单的判断锁对象 markword 里的线程ID是否自己,如果是则直接进入,几乎没有额外开销。

解锁流程:

偏向锁的解锁很简单,就是将 obj 属性赋值为 null,这边很重要的点是不会将锁对象 markword 的线程ID还原回0。
偏向锁流程中,markword 的状态变化如下图所示:

升级轻量级锁流程?

加锁流程:

如果关闭偏向锁,或者偏向锁升级,则会进入轻量级锁加锁流程。
1)从当前线程的栈帧中寻找一个空闲的 Lock Record,obj 属性指向锁对象。
2)将锁对象的 markword 修改为无锁状态,填充到 Lock Rrcord 的 displaced_header 属性。
3)使用 CAS 将对象头的 markword 修改为指向 Lock Record的指针。
此时的线程栈和锁对象的关系如下图所示,可以看到2次锁重入的 displaced_header 填充的是 null。

解锁流程:

1)将 obj 属性赋值为 null。
2)使用 CAS 将 displaced_header 属性暂存的displaced mark word 还原回锁对象的 markword。

升级重量级锁流程?

加锁流程:

当轻量级锁出现竞争时,会膨胀成重量级锁。
1)分配一个 ObjectMonitor,并填充相关属性。
2)将锁对象的 markword 修改为:该 ObjctMonitor 地址 + 重量级锁标记位(10)。
3)尝试获取锁,如果失败了则尝试自旋获取锁。
4)如果多次尝试后还是失败,则将该线程封装成 ObjectWaiter,插入到 cxq 链表中,当前线程进入阻塞状态
5)当其他锁释放时,会唤醒链表中的节点,被唤醒的节点会再次尝试获取锁,获取成功后,将自己从 cxq(EntryList)链表中移除
此时的线程栈、锁对象、ObjectMonitor 之间的关系如下图所示:

ObjectMonitor核心属性


ObjectMonitor() {
    _header       = NULL; // 锁对象的原始对象头
    _count        = 0;    // 抢占该锁的线程数,_count大约等于 _WaitSet线程数 + _EntryList线程数
    _waiters      = 0,    // 调用wait方法后的等待线程数
    _recursions   = 0;    // 锁的重入数
    _object       = NULL; // 指向锁对象指针
    _owner        = NULL; // 当前持有锁的线程
    _WaitSet      = NULL; // 存放调用wait()方法的线程
    _WaitSetLock  = 0 ;   // 操作_WaitSet链表的锁
    _Responsible  = NULL ;
    _succ         = NULL ;  // 假定继承人
    _cxq          = NULL ;  // 等待获取锁的线程链表,竞争锁失败后会被先放到cxq链表,之后再进入_EntryList链接
    FreeNext      = NULL ;  // 指向下一个空闲的ObjectMonitor
    _EntryList    = NULL ;  // 等待获取锁的线程链表,该链表的头结点是获取锁的第一候选者
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; // 标记_owner是指向占用当前锁的线程的指针还是BasicLock,1为线程,0为BasicLock,发生在轻锁升级重锁的时候
    _previous_owner_tid = 0;  // 监视器上一个所有者的线程id
  }

解锁流程:

1)将重入计数器-1,ObjectMonitor 里的 _recursions 属性。
2)先释放锁,将锁的持有者 owner 属性赋值为 null,此时其他线程已经可以获取到锁,例如自旋的线程。
3)从 EntryList 或 cxq 链表中唤醒下一个线程节点。

Lock

在Concurrent包中的锁都是可重入锁(线程已经持有锁,同一个线程再次获取同一个锁是可以再次获取成功的),可重入锁这种设计有效避免死锁出现。

ReentrantLock

Lock是一个接口,其定义如下


public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

常用的方法是lock(),unLock();其中lock()是不可中断的。lockInterruptibly()是可以中断的。

锁的公平性和非公平性

Sync抽象类有两个实现类FairSync 和 NonfairSync 分别对应 公平性锁和非公平性锁


 public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

锁的实现基本原理

Sync父类AbstractQueueSynchronizer(AQS);
AQS实现的锁的功能和Synchronized类似的,有阻塞或者唤醒等功能。需要几个核心要素:

  1. 需要一个state变量,标记锁的状态。对state操作是保证线程安全的,需要用到CAS。

  2. 需要记录当前哪个线程持有锁。

  3. 需要底层支持对一个线程的阻塞或者唤醒操作。

  4. 需要一个队列维护所有阻塞的线程。这个队列必须是无锁队列而且需要保证线程安全。

AQS :


private volatile int state; //记录锁的状态,通过CAS修改state值
private transient Thread exclusiveOwnerThread; //记录锁被哪个线程持有

state取值不仅可以是0、1,还可以大于1,就是为了支持锁的可重入性。
state = 0 时,没有线程持有锁,exclusiveOwnerThread = null;
state = 1 时,有一个线程持有锁,exclusiveOwnerThread = 该线程;
当state > 1 时,说明该线程重入了该锁;
Unsafe类中,提供了阻塞或唤醒线程的一对操作原语。park和unPark。


public native void unpark(Object var1);
public native void park(boolean var1, long var2);

当线程调用park()时候,线程就会被阻塞,在另一个线程调用unPark(Thread t),传入被阻塞的线程,就可以唤醒被阻塞的线程。
AQS使用双向链表和CAS实现一个阻塞队列。

公平锁和非公平锁实现差异

FairSync的acquireQueued(..)和NonfairSync类似的
判断没有锁的时候会进行队列判断,是否属于队列第一个,从而实现公平性。

读写锁

ReentrantReadWriteLock读写锁,读线程和读线程不需要互斥。

Java `synchronized` 关键字高频面试题(原理+场景+底层实现) 文章摘要: Java的synchronized关键字是多线程编程中的核心同步机制,提供原子性、可见性和有序性保证。其使用方式包括修饰实例方法(锁对象为当前实例)、静态方法(锁对象为Class对象)和同步代码块(自定义锁对象)。底层通过对象头(Mark Word)和监视器锁(ObjectMonitor)实现,支持锁升级机制(无锁→偏向锁→轻量级锁→重量级锁)。wait()/notify()必须配合synchronized使用以确保线程安全和正确操作Monitor队列。相比ReentrantLock,synch 阅读详情

相关推荐

Java最全面试题十七:Java高级面试 - 多线程,史上最全的《Java面试题及解析》

JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录。

2401_84557547的博客 357

【面试题】sychronized

Synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全,而Synchronized关键字就是用于代码同步。什么情况下会数据不安全呢,要满足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。修饰静态方法(锁当前类):对当前类的Class对象加锁 修饰实例方法(锁当前对象实例):对当前实例对象this加锁 修饰代码块(锁指定对象/类):synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。 synchronized(类.cla

yzx3105的博客 811

JAVA面试题 简谈你对synchronized关键字的理解

主要介绍了JAVA面试题 请谈谈你对Sychronized关键字的理解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

synchronized 常见面试题总结

在多线程编程中,有时候需要保证多个线程对某个共享资源(如变量、方法或对象)的访问是互斥的,即同一时刻只能有一个线程访问该资源,其他线程需要等待。这种情况下,就需要使用同步机制来实现线程的同步和互斥。Synchronized是Java提供的一种同步机制,它能够保证在同一时刻只有一个线程能够进入synchronized代码块或方法,从而避免了多个线程同时访问共享资源的问题,保证了线程安全和程序的正确性。

m0_51431003的博客 1616

(Linux)DHCP 超级作用域和中继代理(超难受做了超久)

DHCP服务器在192.168.159.0 可以实现分配IP的功能,现在要跨网段分配IP 就需要用到DHCP中继代理。中继代理有两张网卡分别eth0:IP 192.168.159.2 ;eth1: IP:192.168.200.2 。现在要实现192.168.200.0网段的客户机可以获取DHCP服务器的IP。中继代理其实是相当于一个路由器的作用 DHCP服务器的IP地址 :192.168.159...

DJ_joker的博客 2225

5G NR上行控制信道PUCCH

一、PUCCH概述 PUCCH用于承载上行控制信息,相比LTE,NR PUCCH支持5种不同的格式,按照时域上所占用的符号数量可以分为短格式和长格式两种,如下表所示,短格式占用1-2个符号,可以承载1-2bit信息,长格式占用4-14个符号,可承载大于2bit的信息。NR引入短格式PUCCH的目的是可以缩短HARQ-ACK反馈的时延,长格式仍然是考虑到持续时间长可以保证覆盖。 NR中,考虑到系...

一个关于5G NR的博客^^ 3万+

2024年最全全网最硬核的 synchronized 面试题深度解析,java排序面试题

说句实话,如果一个打工人不想提升自己,那便没有工作的意义,毕竟大家也没有到养老的年龄。当你的技术在一步步贴近阿里p7水平的时候,毫无疑问你的薪资肯定会涨,同时你能学到更多更深的技术,交结到更厉害的大牛。推荐一份Java架构之路必备的学习笔记,内容相当全面!!!成年人的世界没有容易二字,前段时间刷抖音看到一个程序员连着加班两星期到半夜2点的视频。在这个行业若想要拿高薪除了提高硬实力别无他法。你知道吗?现在有的应届生实习薪资都已经赶超开发5年的程序员了,实习薪资26K,30K,你没有紧迫感吗?

2401_84585462的博客 436

工创赛智能搬运小车实战:视觉定位、底盘控制与RTOS调度

智能搬运系统是嵌入式控制与机器视觉融合的典型工程实践,其核心在于视觉定位精度、运动控制稳定性与实时任务调度能力。在有限算力(如STM32H7)、低功耗约束和结构化场景下,传统CV方法(如FAST+RANSAC)比轻量级深度学习更具鲁棒性与实时性;而底盘控制需穿透PID表层,直击死区补偿、编码器硬件滤波与负载自适应等物理层细节;RTOS则不是炫技工具,而是保障多模块时序确定性的刚需——尤其在视觉-控制-执行强耦合链路中,任务优先级、事件组通信与栈安全直接影响系统存活。本文聚焦全国大学生工程训练竞赛(工创赛)真

weixin_29194693的博客 277

全网最全synchronized锁升级过程

在面试题中经常会有这么一道面试题,谈一下synchronized锁升级过程?之前背了一些,很多文章也说了,到底怎么什么条件才会触发升级,一直不太明白。实践是检验真理的唯一标准,今天就和大家一起实践一下,什么条件才会升级!经过实战,我们知道了每一个的切换条件,可以在面试中好好的回答了。不至于面试官反问一下就不坚定了!关于切换到重量级锁后,有兴趣的话,可以下载openJDK源码去看一下关于和源码下载地址:是 OpenJDK 中实现 Java同步机制的核心部分,它负责管理对象监视器。

小王博客基地 1875

AHB总线协议

1. 简介         AHB总线规范是AMBA总线规范的一部分,AMBA总线规范是ARM公司提出的总线规范,被大多数SoC设计采用,它规定了AHB (Advanced High-performance Bus)、ASB (Advanced System Bus)、APB (Advanced Peripheral Bus)。AHB用于高性能、高时钟频率的系统结构,典型的应用如ARM核与系统内

ivy_reny的专栏 8万+

VS2022命令行编译C++报错?一招解决‘fatal error C1034: iostream: 不包括路径集‘

本文针对VS2022命令行编译C++时常见的'fatal error C1034: iostream: 不包括路径集'错误,深入剖析了其根源在于环境变量缺失。文章提供了永久配置用户环境变量(PATH、INCLUDE、LIB)的一劳永逸解决方案,确保在任何终端中都能直接使用cl命令进行编译,并给出了进阶配置与常见错误排查指南。

weixin_29011667的博客 305

2024年Java最新EventBus 源码解析及使用体验,史上最全的《Java面试题及解析》

一般像这样的大企业都有好几轮面试,所以自己一定要花点时间去收集整理一下公司的背景,公司的企业文化,俗话说「知己知彼百战不殆」,不要盲目的去面试,还有很多人关心怎么去跟HR谈薪资。这边给大家一个建议,如果你的理想薪资是30K,你完全可以跟HR谈33~35K,而不是一下子就把自己的底牌暴露了出来,不过肯定不能说的这么直接,比如原来你的公司是25K,你可以跟HR讲原来的薪资是多少,你们这边能给到我的是多少?你说我这边希望可以有一个20%涨薪。

2401_84688466的博客 1134

TensorFlow_PyTorch实战:构建AI人工智能回归模型

回归分析是机器学习的核心任务之一,用于预测连续型目标变量(如房价、股票价格、用户留存时间等)。基础线性回归模型的数学原理与代码实现多项式回归模型的过拟合处理与正则化技巧自定义数据集加载与数据预处理流程动态图(PyTorch)与静态图(TensorFlow)的编程范式对比模型评估指标与可视化分析方法核心概念:对比两大框架的架构设计,解析回归模型核心组件数学原理:推导线性回归的假设函数、损失函数及优化算法算法实现:提供TensorFlow和PyTorch的完整代码示例项目实战。

AI天才研究院 1432

2026最全Java面试题总结(含答案)

本文摘要: Java并发编程方面,解释了并行与并发的区别,线程与进程的关系,以及线程池、锁机制等核心概念。Spring框架部分介绍了IoC、AOP原理及常用模块,Spring Boot的自动配置特性。MyBatis章节对比了#{}和${}的区别,分析了一二级缓存机制。设计模式部分列举了单例、工厂等常见模式的实现方式。网络协议方面详细讲解了TCP三次握手、HTTP与HTTPS差异等。此外还涵盖了JVM内存模型、垃圾回收算法、数据库事务隔离级别等Java核心技术点。全文通过实例代码和对比表格,系统性地总结了Ja

2401_89221445的博客 1100

单片机通过蜂鸣器播放任意音乐代码实现(2):音乐单片机代码自动生成

单片机通过蜂鸣器播放任意音乐代码实现(2):音乐单片机代码自动生成 上一节我们已经构建了基于51单片机的蜂鸣器音乐播放环境,接下来只需要手动或者自动添加音乐代码便能实现不同音乐的播放。当然,第二节将告诉你如何自动生成这些音乐代码。 所用软件 MuseScore 3;用于转换MID文件为musicxml文件 C/C++ IDE(本文为Visual Studio 2019) 1.生成音乐对应的MID文件 可以直接下载mid格式音乐文件,也可以将某段音乐转换为MID文件。最好是直接使用mid格式音乐文件,可

kevin1499的博客 2万+

运放+MOS恒流源电路仿真分析

利用运放设计一个恒流源

HW-BASE的博客 4101

最全Java多线程基础(二),java数据库连接池面试题

wait()方法不是必须要传入参数,如果没有参数就等待,如果传入参数就到时间后等待。//随机唤醒另外一条线程。

2301_76348344的博客 1124

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

最全的BAT大厂面试题

一、java面试题 熟练掌握java是很关键的,大公司不仅仅要求你会使用几个api,更多的是要你熟悉源码实现原理,甚至要你知道有哪些不足,怎么改进,还有一些java有关的一些算法,设计模式等等。 (一) java基础面试知识点 java中==和equals和hashCode的区别 int、char、long各占多少字节数 int与integer的区别 谈谈对java多态的理解 String、Str...

IT混混噩噩生活 547

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

最全面试官突击一问,【备战秋招冲击大厂,java面试题及其答案

2,9,1L,七大参数:1)corePoolSize(常驻核心线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。

2401_84584535的博客 754

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

史上最全的Java面试题总汇,成功坐等offer(答案版)上

本人是衔接上文(史上最全的Java面试题总汇,不再惧怕面试官,成功坐等offer)的带答案版本,答案已经整理完毕。希望大家采纳。 上文链接https://blog.youkuaiyun.com/jwd1049044602/article/details/97756441 本文内容偏多,建议收藏阅读。 小编整理不易,给个关注吧~ 面试题如下: 一、Java 基础 1.JDK 和 JRE ...

410

目前最全的《Java面试题及解析》,2024Java大厂面试真题

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)!imageMo。

m0_63102527的博客 422

2024年最全Java多线程面试题,我丝毫不慌(3),2024年最新网络安全高频面试题+解析

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~Lock显式锁是一个接口,我们来看看:随便翻译一下他的顶部注释,看看是干嘛用的:可以简单概括Lock方式来获取锁支持中断、超时不获取、是非阻塞的提高了语义化,哪里加锁,哪里解锁都得写出来Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁支持Condition条件对象允许多个读线程同时访问共享资源synchronized好用,简单,性能不差。

2401_84297796的博客 1359

Java最全【leetcode刷题】1(1),synchronized底层是怎么实现的

这份清华大牛整理的进大厂必备的redis视频、面试题和技术文档祝大家早日进入大厂,拿到满意的薪资和职级~~~加油!!!本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录需要这份系统化的资料的朋友,可以点击这里获取面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.youkuaiyun.com/forums/4f45ff00ff254613a03fab5e56a57acb)收录**需要这份系统化的资料的朋友,可以点击这里获取。

2401_84557547的博客 448

Java框架!史上最全的Java面试题集锦

01 并发宝典:面试专题 面试专题分为四个部分,分别如下 Synchronized 相关问题 可重入锁 ReentrantLock 及其他显式锁相关问题 Java 线程池相关问题 Java 内存模型相关问题 1.1 Synchronized 相关问题(这里整理了八问) 问题一:Synchronized用过吗?其原理是什么? 问题二:你刚才提到获取对象的锁 ,这个“ 锁 ”到底是什么?如何确定对象的锁? 问题 三:什么是可重入性 , 为什么说Synchronized是可重入锁?

m0_57286472的博客 273

史上最全的Java线程面试题

1、java 中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和 和 suspend() 方法为何不推荐使用? java5以前,有如下两种: 第一种: new Thread(){}.start();这表示调用 Thread 子类对象的 run 方法,new Thread(){}表示一个 Thread 的匿名子类的实例对象,子类加上 run 方法后的代码如下: new Thre...

qq_43107323的博客 688

最全多线程经典面试题和答案

Java实现线程有哪几种方式? 1、继承Thread类实现多线程2、实现Runnable接口方式实现多线程3、使用ExecutorService、Callable、Future实现有返回结果的多线程 多线程同步有哪几种方法? Synchronized关键字,Lock锁实现,分布式锁等。 Runnable和Thread用哪个好? Java不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Run...

一个有情怀的程序猿博客 807
上一篇: 万字长文详解Java线程池
下一篇: 八股文-基础知识-面试题汇总(一)
Java_2017_csdn
博客等级 码龄10年 329粉丝 49原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值