深度思考java和OS中的各种锁

推荐

1 CountdownLatch和CyclicBarrier的区别使用场景与具体实现
2 有哪些 Java 面试题 90% 的公司都会问到?

1 OS中的各种锁

1.1 有哪些锁

在操作系统中,锁是用于控制多个进程或线程访问共享资源的基本工具。以下是几种典型的锁及其描述:

  1. 互斥锁 (Mutex Lock):

    • 互斥锁保证任何时候只有一个线程可以持有该锁,从而保证对共享资源的独占访问。
    • 用于保护临界区,防止同一时间有多个线程进入临界区。
  2. 读写锁 (Read-Write Lock):

    • 允许多个线程同时读共享资源,但在写入资源时只允许一个线程。
    • 当某个线程持有写锁时,其他线程不能获取读锁或写锁。
  3. 自旋锁 (Spinlock):

    • 当线程尝试获取自旋锁并失败时,它会不断循环尝试获取锁,而不是进入睡眠状态。这在锁被持有时间较短的情况下是有效的,因为线程可以快速获取锁而不用等待。
    • 适用于锁持有时间短和线程不想在获取锁时睡眠的场景。
  4. 信号量 (Semaphore):

    • 是一个更为通用的同步工具,可以看作是可以持有多个资源的锁。
    • 信号量可以限制对资源的并发访问数,而不仅仅是一个。
  5. 条件变量 (Condition Variable):

    • 常与互斥锁结合使用,允许线程等待某个条件为真以获取锁。
    • 当某个条件不满足时,线程可以被放到休眠状态,直到另一个线程更改了条件并唤醒等待的线程。
  6. 屏障 (Barrier):

    • 用于同步一组线程,使它们在某个点上达到共同的执行点。
    • 当所有线程都到达屏障时,它们都会被释放继续执行。
  7. 读者-写者锁 (Reader-Writer Lock):

    • 类似于读写锁,但更强调“读者优先”或“写者优先”的策略。
    • “读者优先”允许多个读者并发访问资源,直到一个写者来到。而“写者优先”会让写者有更高的优先级。
  8. 递归锁 (Recursive Lock):

    • 允许一个线程多次获取同一个锁,而不会造成死锁。
    • 通常用于需要多次进入临界区的复杂函数或方法。

这些锁的实现和语义可能因操作系统和具体应用而异。选择合适的锁类型取决于具体的同步需求和使用场景。

扫描二维码关注公众号,回复: 16142148 查看本文章

1.2 基本实现原理

在操作系统中,锁的实现通常涉及硬件级的支持、系统调用和高效的数据结构设计。以下是几种常见的锁及其基本实现原理:

  1. 互斥锁 (Mutex Lock)

    • 基本实现: 使用原子操作,如Test-and-SetCompare-and-Swap。这些原子操作是由硬件直接支持的。
    • 当线程尝试获取锁时,它使用原子操作检查锁的状态。如果锁是空闲的,则线程获取锁;否则,线程可能被阻塞或自旋。
  2. 读写锁 (Read-Write Lock)

    • 基本实现: 包含两部分:一个读锁计数器和一个互斥锁。互斥锁用于写操作,读锁计数器用于跟踪当前的读线程数量。
    • 读取时,读锁计数器增加;写入时,首先要获得互斥锁。
  3. 自旋锁 (Spinlock)

    • 基本实现: 当锁被占用时,线程会在一个循环中不断尝试获取锁,而不进入休眠状态。
    • 也是基于原子操作实现的,如Test-and-Set
  4. 信号量 (Semaphore)

    • 基本实现: 通常包含一个整数值和一个等待队列。整数值代表可用资源的数量。
    • 当线程尝试获得资源而资源不足时,线程会被放入等待队列。当其他线程释放资源时,等待队列中的线程可能会被唤醒。
  5. 条件变量 (Condition Variable)

    • 基本实现: 与互斥锁结合使用。条件变量通常包含一个等待队列。
    • 当某个条件不满足时,线程会进入等待状态。当条件变为真时,线程会被唤醒。
  6. 屏障 (Barrier)

    • 基本实现: 屏障通常包含一个计数器和一个等待队列。计数器跟踪已到达屏障的线程数。
    • 当所有线程都到达屏障时,它们都会被释放继续执行。
  7. 读者-写者锁 (Reader-Writer Lock)

    • 基本实现: 与读写锁类似,但可能包括其他策略,如优先权策略,来决定何时允许读或写操作。
  8. 递归锁 (Recursive Lock)

    • 基本实现: 与互斥锁类似,但它还跟踪锁的拥有者和锁的持有计数。
    • 当当前持有锁的线程再次尝试获取锁时,持有计数增加而不是被阻塞。

这些锁的实现会涉及到操作系统的调度策略、上下文切换和内存管理。锁的选择和使用也会受到程序的同步模式、性能需求和应用场景的影响。

1.3 为什么线程的五种状态中,需要有就绪态,需要有就绪队列?

答:根本原因是cpu的核数小于“等待条件已经满足,已经准备好执行”的线程数,所以不得不把这种就绪态的线程放到一个队列中存起来,这个队列也就取名为就绪队列

2 一些锁的基本信息

2.1 线程阻塞时是否占用资源

线程阻塞时仍会占用一些资源:

  • 内存资源:线程的栈、局部变量、引用的对象等仍然保留在内存中。
  • 系统资源:线程管理相关的资源,例如线程控制块(Thread Control Block, TCB)。
    不过,被阻塞的线程不会消耗 CPU 时间片,因此不会对 CPU 造成负担。

2.2 是不是OS层面上涉及到线程阻塞一定会涉及到切换为内核态?

通常,线程的阻塞和唤醒确实涉及系统调用,这可能会导致从用户态切换到内核态。但有些高级的同步操作可能使用 “spin-wait” 或 “busy-wait” 技术,这些操作会在用户态自旋,尝试多次获取锁,而不是直接进入阻塞状态(可能多次失败后才进入到内核),从而避免用户态到内核态的切换。

2.3 创建、销毁和执行线程都是在内核态中实现的吗:

创建和销毁线程涉及到操作系统资源的分配和回收,通常需要进行系统调用,这会涉及到用户态到内核态的切换。线程的执行主要在用户态进行,但当线程进行一些系统调用或触发某些事件(如页面错误)时,会切换到内核态。

3 java中的锁

3.1 Object#wait()和Object#notify()和AQS定义的几种锁的实现,作用上的区别

- `Object#wait()` 和 `Object#notify()` 是 Java 对象监视器的基本方法,用于线程间的通信。它们允许一个线程等待特定的条件,并允许另一个线程通知该条件的改变。
- AQS 是一个框架,用于构建具有阻塞和超时等待的同步器,如 `ReentrantLock`、`Semaphore`、`CountDownLatch` 等。

主要的差异在于:
- AQS 提供了一个更为复杂和灵活的同步工具构建框架,支持独占和共享模式,而 `Object#wait()` 和 `Object#notify()` 更为基础。
- AQS 定义的锁(如 `ReentrantLock`)允许更高的可扩展性和灵活性,例如尝试获取锁、带超时的锁等待,或者查询锁状态。
- 使用 AQS 定义的锁和同步工具,你可以控制更多的行为,而 `Object#wait()` 和 `Object#notify()` 提供的是一套比较基础的机制。

总的来说,选择哪种方式取决于你的需求。对于简单的线程间通信,Object#wait()Object#notify() 可能就足够了;但对于更复杂的同步需求,AQS 提供的工具会更为合适。

3.2 java中实现的一些应用层的锁,比如AQS下的各种,会最终使用系统调用嘛,比如是否会切换到内核态?

AQS 是 Java 并发包中的一个框架,用于构建锁和同步器。其底层使用了 java.util.concurrent.locks.LockSupport 类的 park() 和 unpark() 方法。在大多数情况下,这些方法在阻塞和唤醒线程时不会触发系统调用,因为它们使用了所谓的 “spin-wait” 或 “busy-wait” 技术。只有当线程需要真正地被阻塞时,park() 才会涉及系统调用,从而可能引起用户态到内核态的切换。

3.3 synchronized 是否使用系统调用:

synchronized 的实现在 JVM 的早期版本中,当线程不能获得锁时,确实会涉及到内核态的切换。但随着 JVM 的发展和优化,现代的 JVM 采用了多种策略来优化 synchronized,如偏向锁、轻量级锁和重量级锁。

  • 偏向锁:当一个锁被线程获取,但没有竞争时,JVM 会将锁标记为偏向锁,并记录这个线程的 ID。当同一个线程再次尝试锁定时,它可以快速获得该锁。这一步不涉及系统调用。
  • 轻量级锁:当有小规模的锁竞争时,JVM 会使用自旋锁的技术,使得线程在用户态自旋等待锁释放。这也不涉及系统调用。
  • 重量级锁:当锁竞争激烈时,JVM 会将锁膨胀为重量级锁,此时涉及到线程阻塞和唤醒,可能会涉及系统调用,从而触发用户态到内核态的切换。

3.4 Object#wait()、Object#notify() 是否使用系统调用

Object#wait() 和 Object#notify() 方法在 JVM 内部都是通过内部的 ObjectMonitor 实现的。当线程调用 wait() 并需要等待时,它实际上会被阻塞。与此相似,notify() 或 notifyAll() 被调用时,会唤醒正在等待的线程。在这些场景下,涉及到线程的阻塞和唤醒,因此可能会涉及系统调用,并可能导致从用户态切换到内核态。但具体的实现细节和优化可能因 JVM 版本和平台而异。

3.5 CountdownLatch的应用场景是分布式事务嘛?

CountDownLatch是Java并发编程中的一个同步辅助类,它允许一个或多个线程等待直到一组操作执行完毕。它经常用于控制并发线程的启动和完成,但不是专门设计用于分布式事务的。

当我们谈到分布式事务时,我们通常指的是在多个不同的系统或服务中协调和执行的事务。例如,一个分布式事务可能涉及到多个微服务中的数据库操作。在这种情况下,我们通常使用两阶段提交(2PC)、三阶段提交或某种补偿机制来确保分布式事务的原子性。

然而,你可以在某些特定场景下使用CountDownLatch来模拟简单的分布式协调。以下是一个非常简化的例子,说明如何使用CountDownLatch来等待多个线程完成他们的任务(这里模拟的是多个服务的操作):

import java.util.concurrent.CountDownLatch;

public class DistributedTransactionDemo {
    
    

    public static void main(String[] args) {
    
    
        int numberOfServices = 3;
        CountDownLatch latch = new CountDownLatch(numberOfServices);

        // 模拟三个不同的服务操作
        new Thread(() -> {
    
    
            System.out.println("Service 1 is processing...");
            // 执行一些操作...
            latch.countDown();
        }).start();

        new Thread(() -> {
    
    
            System.out.println("Service 2 is processing...");
            // 执行一些操作...
            latch.countDown();
        }).start();

        new Thread(() -> {
    
    
            System.out.println("Service 3 is processing...");
            // 执行一些操作...
            latch.countDown();
        }).start();

        try {
    
    
            // 主线程等待其他三个服务操作完成
            latch.await();
            System.out.println("All services have finished processing. Committing transaction...");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
            System.out.println("Transaction failed. Rolling back...");
        }
    }
}

需要注意的是,这只是一个简化的例子,真正的分布式事务处理要复杂得多,通常需要特定的中间件或框架(如Saga模式、TCC、两阶段提交等)来协调和处理。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/132183480
今日推荐