java面试题-JUC锁

1.介绍下LockSupport?

LockSupport 是 Java 并发包中的一个工具类,用于创建锁和其他同步类的基本线程阻塞原语。它也是 J.U.C 中的一个核心基础类。

相较于 Object.wait()Object.notify()LockSupport 可以更加灵活地对线程进行阻塞和唤醒操作,其主要原理是为每个线程关联一个许可(permit)计数器,该计数器的初始值为 0,当线程调用 LockSupport.park() 方法时,如果许可计数器的值为 0,线程会被阻塞挂起,否则许可计数器的值减 1,线程可以正常执行。如果许可计数器的值大于 0,调用 LockSupport.unpark(Thread t) 方法可以将其值加 1,唤醒线程 t。

在 J.U.C 中,很多并发工具都是基于 LockSupport 来实现的,如 ReentrantLockSemaphore 等,因此 LockSupport 的重要性不言而喻。

2.通过wait/notify实现同步?

在 Java 中,wait() 和 notify() 是 Object 类的两个方法,用于线程之间的通信和同步。可以通过它们实现线程间的协调和互斥。

wait() 方法会让当前线程进入等待状态,并释放对象锁。调用 wait() 方法的线程会进入等待集合中,等待被唤醒。notify() 方法会唤醒等待集合中的某个线程。notifyAll() 方法则会唤醒等待集合中的所有线程。

下面是一个通过 wait/notify 实现同步的示例:

public class Worker {
    private Object lock = new Object();
    private int count = 0;

    public void increment() throws InterruptedException {
        synchronized (lock) {
            // 判断是否已经到达最大值
            while (count >= 100) {
                lock.wait(); // 等待
            }

            // 执行任务
            count++;
            System.out.println("increment: " + count);

            lock.notifyAll(); // 唤醒所有等待线程
        }
    }

    public void decrement() throws InterruptedException {
        synchronized (lock) {
            // 判断是否已经到达最小值
            while (count <= 0) {
                lock.wait(); // 等待
            }

            // 执行任务
            count--;
            System.out.println("decrement: " + count);

            lock.notifyAll(); // 唤醒所有等待线程
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();

        // 创建多个线程对共享变量进行操作
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 50; i++) {
                    worker.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                for (int i = 0; i < 50; i++) {
                    worker.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
    }
}

在这个示例中,Worker 类中的 increment() 和 decrement() 方法都包含了一个 while 循环,不断检查共享变量 count 是否满足条件。如果不满足条件,就调用 wait() 方法进入等待状态。如果满足条件,则执行任务,然后调用 notifyAll() 方法唤醒所有等待线程。同时,在主线程中启动多个线程对共享变量进行操作,实现了对共享变量的同步。

3.通过LockSupport的park/unpark实现同步?

LockSupport是一个工具类,提供了阻塞和唤醒线程的方法。park方法可以使当前线程阻塞,而unpark方法可以唤醒一个被阻塞的线程。

使用LockSupport实现同步的一般步骤如下:

  1. 定义一个共享的变量,用于线程之间的通信。

  1. 在生产者线程中,当满足某个条件时,调用LockSupport的unpark方法,唤醒消费者线程。

  1. 在消费者线程中,当满足某个条件时,调用LockSupport的park方法,使线程阻塞等待生产者线程的唤醒。

下面是一个使用LockSupport实现同步的示例代码:

import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {
    static Thread producerThread, consumerThread;
    static volatile boolean flag = false;
    
    public static void main(String[] args) {
        producerThread = new Thread(() -> {
            System.out.println("Producer is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            LockSupport.unpark(consumerThread);
            System.out.println("Producer unpark the consumer thread.");
        });
        consumerThread = new Thread(() -> {
            System.out.println("Consumer is waiting...");
            while (!flag) {
                LockSupport.park();
            }
            System.out.println("Consumer received the signal and resumed.");
        });
        producerThread.start();
        consumerThread.start();
    }
}

在这个示例中,主线程启动了一个生产者线程和一个消费者线程。生产者线程首先等待一段时间,然后将共享的变量flag设置为true,并调用LockSupport的unpark方法唤醒消费者线程。消费者线程在开始时调用LockSupport的park方法阻塞等待生产者线程的唤醒。一旦生产者线程唤醒了消费者线程,消费者线程就会检查共享变量flag的值,如果为true,则表示生产者线程已经完成了工作,消费者线程可以结束执行。

4.Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别?

  1. Thread.sleep()

Thread.sleep() 方法可以让当前线程暂停一段时间,以毫秒为单位,不会释放锁,也不会失去对CPU的控制权。在等待的时间过去之后,线程会重新开始执行。

  1. Object.wait()

Object.wait() 方法会使当前线程等待,直到其他线程调用了相同对象的 notify() 或 notifyAll() 方法。调用 wait() 方法的线程会释放对象锁,以允许其他线程执行 synchronized 方法或块。在线程重新获得锁并被唤醒后,它会重新开始执行。

  1. Condition.await()

Condition.await() 方法是基于 Lock 实现的条件等待方法。与 wait() 不同,await() 必须在获取 Lock 对象之后才能调用。它会释放 Lock 对象并使当前线程等待,直到其他线程调用了 Condition 对象的 signal() 或 signalAll() 方法,唤醒等待的线程并重新获取 Lock 对象以继续执行。

  1. LockSupport.park()

LockSupport.park() 方法可以阻塞当前线程,直到被唤醒。与 wait() 和 await() 不同,park() 方法不需要在 synchronized 块中调用,并且可以在获取锁之前调用。它不会释放任何锁。可以使用 park() 和 unpark() 方法实现更灵活的线程同步。

总的来说,这几种方法的区别在于它们所使用的机制以及对线程和锁的控制权的处理方式。 Thread.sleep() 方法是一个简单的暂停方法,不涉及线程同步或锁;而 Object.wait() 和 Condition.await() 是基于锁的等待机制,需要在 synchronized 块中使用,并与 notify() 或 notifyAll() 方法一起使用。而 LockSupport.park() 和 unpark() 方法可以更灵活地控制线程的暂停和唤醒,可以不需要获取锁并随时使用,但也需要进行合适的配对以避免线程被永久阻塞。

5.如果在wait()之前执行了notify()会怎样?

如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;

如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。

假设有两个线程 A 和 B,它们共享一个对象 obj。线程 A 在 obj 上执行了 obj.wait() 方法并进入了等待状态,此时线程 A 释放了 obj 对象的锁。此时线程 B 获得了 obj 对象的锁并执行了 obj.notify() 方法,因为线程 A 此时已经进入了等待状态,所以线程 B 的通知信号将被记录在 obj 对象的内部,并且在下一次线程 A 获得 obj 对象的锁并执行 obj.wait() 方法时被传递给它,使其从等待状态中恢复并继续执行。

但是,如果线程 B 在线程 A 调用 obj.wait() 方法之前执行了 obj.notify() 方法,此时线程 A 尚未进入等待状态,因此 obj 对象内部并没有记录任何通知信号。在这种情况下,线程 B 的通知信号将被忽略,并且不会被记录。因此,即使线程 A 调用了 obj.wait() 方法,也没有任何通知信号能够被传递给它,因此它仍然会一直处于等待状态。

所以,正确使用 wait() 和 notify() 方法需要注意它们的顺序,以确保线程通信的正确性和避免死锁等问题的发生。

6.如果在park()之前执行了unpark()会怎样?

如果在park()方法之前调用了unpark()方法,这个线程调用park()方法时将会立即返回,并且不会被阻塞。同时,调用unpark()方法会向该线程授予一个许可证,使得该线程在以后调用park()方法时不会被阻塞。这是因为unpark()方法并不需要线程处于阻塞状态才能调用,它只是授予许可证,如果许可证尚未被使用,则该线程在未来调用park()方法时将会立即返回而不会被阻塞。


7.什么是AQS? 为什么它是核心?

AQS(AbstractQueuedSynchronizer)是Java中的一个重要类,它是实现锁、信号量、倒计时计数器等同步机制的基础。AQS提供了一种通用的同步框架,可以很容易地扩展出各种不同类型的同步器,例如ReentrantLock、CountDownLatch、Semaphore等等。

AQS的核心是一个FIFO队列,称为等待队列,它用于保存等待获取锁或许可证的线程。当一个线程想要获取锁或许可证时,如果当前已经被其他线程持有,则该线程将被放入等待队列中,并阻塞线程。当持有锁或许可证的线程释放它们时,AQS会从等待队列中取出一个线程,并将其唤醒,使其可以继续执行。

AQS之所以成为Java并发编程的核心,是因为它提供了一种高效、灵活、可扩展的同步机制。相比较于synchronized关键字,AQS提供的ReentrantLock类可以实现更加灵活的锁,例如支持公平锁和非公平锁、可重入锁、可中断锁等等。此外,AQS还提供了Condition等新的同步工具,可以更加方便地实现线程通信和线程的等待和唤醒。

另外,AQS提供了基于CAS(Compare-And-Swap)操作的原子性操作,这使得AQS在实现同步器时可以避免使用传统的互斥锁机制,从而减少线程阻塞和切换的开销,提高了并发执行的效率。

8.AQS的核心思想是什么?

AQS的核心思想是“状态同步并阻塞”,即通过维护一个状态来协调多个线程的执行,当状态不满足某个条件时,线程会被阻塞,等待状态满足条件时被唤醒。这个状态可以用一个int类型的变量来表示,它的值代表了同步对象的状态,如锁是否被占用、可用资源的数量等等。

在AQS中,通过实现tryAcquire(int arg)和tryRelease(int arg)方法来控制同步对象的状态,并且通过等待队列来维护等待获取同步对象的线程。当一个线程请求获取同步对象时,tryAcquire方法会根据同步对象的状态来决定是否允许该线程获取同步对象。如果tryAcquire返回false,说明同步对象已经被其他线程占用,此时该线程会被加入等待队列并被阻塞。当同步对象的状态发生变化时,如锁被释放,tryRelease方法会将等待队列中的线程唤醒,重新竞争同步对象。

AQS的核心思想使得它可以实现各种不同类型的同步器,例如独占锁、共享锁、信号量、倒计时计数器等等。同时,AQS还支持公平锁和非公平锁、可重入锁、可中断锁等特性,这使得它成为了Java并发编程中非常重要的同步机制。

9.AQS有哪些核心的方法?

  1. acquire(int arg):尝试获取同步状态,如果获取失败则阻塞当前线程。

  1. acquireInterruptibly(int arg):尝试获取同步状态,如果获取失败则阻塞当前线程,并响应中断。

  1. tryAcquire(int arg):尝试获取同步状态,如果获取成功则返回true,否则返回false。

  1. release(int arg):释放同步状态。

  1. acquireShared(int arg):尝试获取共享同步状态,如果获取失败则阻塞当前线程。

  1. tryAcquireShared(int arg):尝试获取共享同步状态,如果获取成功则返回非负数,否则返回负数。

  1. releaseShared(int arg):释放共享同步状态。

这些方法是AQS实现各种同步器的基础,具体的同步器会在这些方法的基础上进行扩展和实现。

10.AQS定义什么样的资源获取方式?

AQS定义了独占模式和共享模式两种资源获取方式。

独占模式是指在同一时刻只能有一个线程获取到资源,其他线程需要等待。比如独占锁就是一种独占模式的同步器。在AQS中,独占模式的资源获取方式由tryAcquire(int arg)tryRelease(int arg)方法实现。

共享模式是指多个线程可以同时获取资源,但有限制条件。比如读写锁就是一种共享模式的同步器,多个线程可以同时读取资源,但只有一个线程可以写入资源。在AQS中,共享模式的资源获取方式由tryAcquireShared(int arg)tryReleaseShared(int arg)方法实现。

11.AQS底层使用了什么样的设计模式?

AQS底层使用了模板方法模式和回调模式这两种设计模式。

模板方法模式是一种行为型设计模式,通过定义一个算法框架,具体的子类可以通过覆盖某些步骤来实现自己的逻辑。AQS的acquirerelease等方法就是模板方法,由具体的子类通过实现tryAcquiretryRelease等方法来定义自己的逻辑。

回调模式是一种事件驱动型设计模式,通过在对象之间建立一种“回调关系”,当一个对象的状态发生变化时,另一个对象会自动得到通知。AQS的等待队列就是通过回调模式来实现的,当一个线程需要等待时,会被封装成一个等待节点,加入等待队列中,并通过回调机制被唤醒。


12什么是可重入,什么是可重入锁? 它用来解决什么问题?

可重入:同一个线程可以多次获得同一个锁或者同步资源,而不会造成死锁或者其他异常情况的发生。Java中的synchronized关键字就是可重入的,同一个线程可以多次获取同一个锁,而不会出现死锁或者锁被其它线程持有的情况。

可重入锁:是一种特殊的锁,它允许同一个线程多次获取同一个锁,并且在释放锁时需要释放相同次数的锁。ReentrantLock就是一种可重入锁的实现,它可以通过lock()方法获取锁,同时可以通过unlock()方法释放锁。如果同一个线程多次获取了该锁,则需要多次调用unlock()方法才能完全释放锁。

可重入锁的作用主要是解决线程的嵌套调用问题,避免死锁的发生。在多线程编程中,经常会出现方法中调用其他方法的情况,如果这些方法需要获取同一把锁,则有可能出现死锁的情况。使用可重入锁可以避免这种情况的发生,提高程序的健壮性和并发性。

13.ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?

ReentrantLock确实是基于AQS来实现的,但是它并没有通过继承AQS来实现。ReentrantLock实际上是使用了AQS的模板方法模式,在AQS提供的基础上实现了自己的锁逻辑。

具体来说,ReentrantLock在AQS的基础上实现了两个内部类:Sync和FairSync,分别代表非公平锁和公平锁。这两个内部类都继承自AQS,并分别实现了AQS中的tryAcquire和tryRelease等方法,以及自己独有的逻辑。通过这种方式,ReentrantLock实现了自己的锁逻辑,并且可以根据需要选择公平或非公平的锁。

需要注意的是,由于ReentrantLock是使用了AQS的模板方法模式,因此它对AQS的实现细节高度依赖,对于使用者而言,更多地是通过ReentrantLock提供的接口来使用和操作锁,而不是直接使用AQS的接口。

14.ReentrantLock是公平还是非公平锁?

ReentrantLock默认实现的是非公平锁。在创建ReentrantLock对象时,如果不指定参数,则会创建一个非公平锁。如果要创建公平锁,需要通过构造函数的参数指定fair值为true。例如:

ReentrantLock lock = new ReentrantLock(true); // 创建公平锁

ReentrantLock通过内部类FairSync和NonfairSync来实现公平锁和非公平锁,FairSync是公平锁的实现,NonfairSync是非公平锁的实现。FairSync和NonfairSync都继承了AQS同步器类Sync,它们实现了Sync中的tryAcquire方法和tryRelease方法,实现了获取锁和释放锁的逻辑。

15.ReentrantLock和ReentrantReadWriteLock的区别?

ReentrantLock和ReentrantReadWriteLock都是可重入锁,可以支持线程的互斥访问和同步。

不同之处在于,ReentrantLock是独占锁,即同一时间只能有一个线程获得锁并访问共享资源,而ReentrantReadWriteLock支持两种锁:读锁和写锁。在读锁存在的情况下,多个线程可以同时获取读锁,但在写锁存在的情况下,只有一个线程可以获取写锁,其他线程无法获取读锁或写锁。

因此,ReentrantLock适用于互斥访问的场景,ReentrantReadWriteLock适用于读写操作频繁的场景。在读操作远远多于写操作的情况下,使用ReentrantReadWriteLock可以提高并发性和吞吐量。但是,在写操作较多的情况下,如果多个线程争夺写锁,会导致其他线程无法获取读锁或写锁,从而降低并发性能。

16.ReentrantReadWriteLock底层实现原理?

ReentrantReadWriteLock包含了五个内部类,它们之间相互关联,用于实现读写锁的不同语义和行为:

  1. Sync:ReentrantReadWriteLock的基础同步器,包括读锁和写锁的获取、释放等操作,是所有内部类的父类。

  1. NonfairSync:非公平同步器,继承自Sync,实现了非公平的读写锁。

  1. FairSync:公平同步器,继承自Sync,实现了公平的读写锁。

  1. ReadLock:读锁类,包括获取读锁、释放读锁等操作,通过封装Sync类来实现。

  1. WriteLock:写锁类,包括获取写锁、释放写锁等操作,通过封装Sync类来实现。

其中,Sync是ReentrantReadWriteLock的基础同步器,NonfairSync和FairSync分别实现了非公平锁和公平锁,而ReadLock和WriteLock则是对Sync的封装,提供了方便的读锁和写锁的获取和释放方法。这些类之间的关系构成了ReentrantReadWriteLock的内部实现机制。

17.ReentrantReadWriteLock底层读写状态如何设计的?

ReentrantReadWriteLock底层的读写状态是通过一个32位的整型变量来实现的,其中高16位表示读状态,低16位表示写状态。具体来说,高16位的每一位表示是否有读锁被占用,低16位的每一位表示是否有写锁被占用。

18.ReentrantReadWriteLock读锁和写锁的最大数量是多少?

ReentrantReadWriteLock中,读锁和写锁的最大数量取决于32位整型变量中的位数。因为高16位表示读锁的状态,每个位表示一个读锁是否被占用,所以理论上最大支持65535(2的16次方-1)个读锁。同样地,低16位表示写锁的状态,每个位表示一个写锁是否被占用,因此最大支持65535个写锁。

但是,在实际应用中,支持这么多读写锁是没有必要的,也可能会导致性能下降。一般情况下,ReentrantReadWriteLock默认支持最多65535个读锁和一个写锁。但是,这个数量可以通过构造函数进行调整。

19.本地线程计数器ThreadLocalHoldCounter的作用?

ThreadLocalHoldCounter是ReentrantReadWriteLock的一个内部类,用于实现可重入的读锁。每个线程在获取读锁时会调用Sync的tryAcquireShared方法,如果已经持有了读锁,则将对应的计数器加1,如果没有持有读锁,则尝试获取读锁,并创建一个计数器保存到ThreadLocal中。使用ThreadLocalHoldCounter可以避免线程之间的竞争,提高了读锁的性能,并保证了线程安全。

20.写锁的获取与释放是怎么实现的?

ReentrantReadWriteLock中的写锁获取和释放是通过调用AQS的tryAcquiretryRelease方法实现的。当写锁被请求时,会尝试调用tryAcquire获取锁,如果获取锁成功,则更新同步状态并返回true;如果获取锁失败,则返回false。当写锁被释放时,会尝试调用tryRelease释放锁,如果释放成功,则更新同步状态并返回true;如果释放失败,则返回false。在释放写锁的时候,如果当前线程持有的写锁数量变成了0,那么会通过调用fullRelease方法唤醒等待队列中的读线程。

21.读锁的获取与释放是怎么实现的?

ReentrantReadWriteLock中的读锁获取和释放是针对读锁计数器进行操作的。当读锁被请求时,会尝试调用tryAcquireShared获取锁,如果获取锁成功,则更新同步状态并返回非负整数;如果获取锁失败,则返回负整数。当读锁被释放时,会尝试调用tryReleaseShared释放锁,如果释放成功,则更新同步状态并返回true;如果释放失败,则返回false。在释放读锁的时候,如果当前线程持有的读锁数量变成了0,那么会通过调用tryComplete方法唤醒等待队列中的写线程。

22.RentrantReadWriteLock为什么不支持锁升级?

ReentrantReadWriteLock 不支持锁的升级,即不能从读锁直接升级为写锁,原因是升级可能会导致死锁或降低并发性能。

假设有一个线程持有读锁,并且想要升级为写锁,需要先释放读锁,再尝试获取写锁。但是,当此时有其他线程已经获取了写锁,那么升级的线程就会被阻塞,等待写锁释放。此时,如果另外的线程持有了读锁,那么就会出现死锁的情况。

此外,锁的升级还可能导致降低并发性能。因为升级操作需要先释放读锁,再获取写锁,这中间存在一段时间窗口,其他线程可以获取写锁,从而降低了并发性能。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值