最全的 synchronized 面试题解析

你认识多少种锁?

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

  • 可重入锁/非可重入锁

  • 共享锁/独占锁

  • 公平锁/非公平锁

  • 悲观锁/乐观锁

  • 自旋锁/非自旋锁

  • 可中断锁/不可中断锁

锁分别有什么特点?

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

这三种锁特指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 指令实现加锁及释放锁的。

为什么调用Objectwait 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)当其他锁释放时,会唤醒链表中的节点,被唤醒的节点会再次尝试获取锁,获取成功后,将自己从 cxqEntryList)链表中移除
此时的线程栈、锁对象、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读写锁,读线程和读线程不需要互斥。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值