【Java】Java核心要点总结:60

简介: 1. 乐观锁一定就是好的吗乐观锁并不一定好,它有自己的适用场景和局限性。乐观锁的优点在于在低并发环境下表现良好,操作基本上都能够成功,不会阻塞其他线程的执行。此外,乐观锁没有锁带来的资源竞争和多线程间的上下文切换开销,对于高并发的场景下可以提供更好的性能表现。但是,在高并发场景下乐观锁的表现往往受到影响,因为多个线程同时执行操作和检查比较,会出现重试次数增加的情况,进而增加了延迟和开销,降低了系统的吞吐量和性能。此时,悲观锁可能比乐观锁更适合解决问题,因为它能够阻塞其他线程,等待当前线程释放锁后再执行。

1. 乐观锁一定就是好的吗


乐观锁并不一定好,它有自己的适用场景和局限性。


乐观锁的优点在于在低并发环境下表现良好,操作基本上都能够成功,不会阻塞其他线程的执行。此外,乐观锁没有锁带来的资源竞争和多线程间的上下文切换开销,对于高并发的场景下可以提供更好的性能表现。


但是,在高并发场景下乐观锁的表现往往受到影响,因为多个线程同时执行操作和检查比较,会出现重试次数增加的情况,进而增加了延迟和开销,降低了系统的吞吐量和性能。此时,悲观锁可能比乐观锁更适合解决问题,因为它能够阻塞其他线程,等待当前线程释放锁后再执行。


因此,选择使用何种锁策略需要根据实际情况进行综合考虑,包括需求、应用场景、并发程度以及编写代码的复杂程度等多个因素。在实际开发中也可以尝试将两种锁策略结合起来使用,根据情况动态选择最适合的策略,提升代码的效率和性能表现。

2. Synchronized和ReentrantLock有什么异同

Synchronized 和 ReentrantLock 在实现线程同步的过程中是很类似的,它们都可以通过获得锁控制并发访问,但也有一些不同点:


相关性:Synchronized 是 Java 语言内置的关键字,因此使用起来非常简便;而 ReentrantLock 是一个基于 Java API 的类,需要手动创建和管理。


获取方式:Synchronized 锁时很容易获取的,在使用 synchronized 关键字修饰方法或代码块时,都自动获得了对象的锁;而 ReentrantLock 需要显式地调用 lock() 锁定资源,并且在使用后还需要 unlock() 解锁,否则就会发生死锁等问题。


可扩展性:ReentrantLock 拥有 Synchronized 不具备的额外功能,比如支持可重入锁、公平锁和非阻塞锁等。


性能差异:相较于 Synchronized,ReentrantLock 执行效率更快,特别是在遇到高并发场景时,ReentrantLock 更容易保证线程执行的顺序。但是,当并发量较小的时候,Synchronized 的效率可能会更高,此外 Synchronized 内部包装了锁的升级降级机制,使得效率在低竞争、低并发的情况下也比较高。


在选择 Synchronized 和 ReentrantLock 的时候,需要根据具体的实现场景和需求来选择合适的锁策略,两者都有其优势和适用场地,不能单纯的从性能方面进行取舍判定。并且,在锁的使用中一定要注意避免死锁等问题的发生,保证线程安全。

3. ReentrantLock是如何实现可重入性的

ReentrantLock 实现可重入性的核心在于内部维护了一个 holdCount 属性,用于记录锁被当前线程持有的次数。初次获得锁时,这个值为 1,每次进入同步块时加 1,退出时减 1。只有当 holdCount 的值为 0 时,表示锁已被完全释放。

实现代码大致如下:

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        lock.lock();
        try {
            // do something
            method2();
        } finally {
            lock.unlock();
        }
    }
    private void method2() {
        lock.lock();
        try {
            // do something
        } finally {
            lock.unlock();
        }
    }
}

在上面的代码中,method() 方法获取了锁后又调用了 method2() 方法,而 method2() 方法也需要获取同一个锁。因为当前线程已经获得了该锁,所以可以再次获取,这就是 ReentrantLock 可重入的原因。


此外,ReentrantLock 还有许多高级特性,比如公平锁、读写锁等,它们都基于可重入锁的思想衍生而来,充分利用了 ReentrantLock 所提供的灵活性和可扩展性。

4. 和Synchronized相比,可以重入锁ReentrantLock实现原理有什么不同


和 Synchronized 相比,重入锁 ReentrantLock 的实现原理有一些不同:


底层实现:ReentrantLock 是基于 Java API 实现的类,而 Synchronized 则是语言级别支持的关键字。在底层实现上,ReentrantLock 基于 AbstractQueuedSynchronizer (AQS)来实现线程同步,而 Synchronized 则是直接依赖 JVM 实现的。


获得和释放锁的方式:ReentrantLock 提供了 lock() 和 unlock() 方法,用于手动获取和释放锁,同时也提供 tryLock() 方法来尝试获得锁;而 Synchronized 则是由 JVM 自动管理的,当线程进入 synchronized 修饰的代码块时自动获取监视器锁,退出时自动释放。


重入性:ReentrantLock 可以支持可重入锁机制,即线程可以多次获取同一把锁,而 Synchronized 已经默认支持了重入锁机制。


可定制性:ReentrantLock 有更高的可定制性,如公平锁、非公平锁、可实现读写分离等特殊场景下需要使用的扩展功能;而 Synchronized 则不具备这些扩展功能。


性能表现:在低竞争、低并发的情况下,Synchronized 不比 ReentrantLock 差,在高并发、高竞争的情况下,ReentrantLock 的性能优于 Synchronized。


ReentrantLock 和 Synchronized 都可以用于实现线程同步,但 ReentrantLock 的可定制性和可重入性底层基于 AQS 实现,在某些高并发场景下表现更优;而 Synchronized 的使用成本低,实现简单,适合在一些简单场景下使用。

5. AQS框架

AQS(AbstractQueuedSynchronizer)是一个提供了一个简单框架用于实现阻塞锁和相关的同步器的抽象类。


AQS 的核心思想就是通过一个双向队列来完成线程的排队和等待,它维护了一个 Volatile 的 state 变量,可以理解为 status。state 的修改只能由持有独占锁的线程完成,其他线程可以通过 CAS 操作竞争修改它,以此控制对共享资源的访问和保证线程间的互斥访问。


通过这种方式,AQS 屏蔽了大部分与并发操作相关的细节,并提供了基础的同步操作,如 acquire(获取锁) 和 release(释放锁),同时还可以扩展出诸如 Semaphore、CountDownLatch、ReentrantLock 等高级同步器,这些同步器都是基于 AQS 框架实现的。


在使用 AQS 时,开发者需要负责编写 acquire 和 release 方法来实现同步控制逻辑,通常会通过一个内部类来代表各个需要同步的状态,如 Sync 类在 ReentrantLock 中就代表了_HOLD、_WAITING 和 _NONE 三个状态,然后针对不同的状态下具体的执行逻辑编写相应的 acquire 和 release 方法,在 acquire 方法中尝试获取锁,如果失败则加入等待队列并挂起自己,等到获取到锁或者被中断时再次尝试获取并执行相应的操作;在 release 方法中释放锁并唤醒等待队列中的线程。


AQS 是一个基于队列和状态变量实现的同步框架,屏蔽了底层调度和同步机制的细节,提供了一套标准的用于实现阻塞锁和同步器的 API,并通过各种扩展支持了更多高级同步器的实现。

相关文章
|
11月前
|
安全 Java 数据库连接
【Java】Java核心要点总结:62
1. 线程中的线程是怎么创建的,是一开始就随着线程池的启动创建好的吗? 线程中的线程通常是通过在父线程中创建新的Thread对象并将其加入线程池中实现的。当然,这个过程也可以通过一些其他的方式来实现,比如使用ExecutorService.submit()方法提交一个Callable或Runnable任务。
|
11月前
|
缓存 安全 Java
【Java】Java核心要点总结70
1. volatile 如何保证变量的可⻅性? 在Java中,使用volatile关键字可以确保变量的可见性。 当一个线程修改了一个被volatile修饰的变量时,它会立即将该变量的最新值刷新到主内存。而其他线程在读取该变量时,会从主内存中重新获取最新值,而不是使用缓存中的旧值。 这样做的原因是,普通的变量在多线程环境下存在线程间不可见的问题。每个线程都有自己的工作内存,由于运行速度快和编译优化等原因,线程可能会直接读取工作内存中的旧值,而不去主内存中获取最新的值,导致线程之间的数据不一致。
|
存储 网络协议 JavaScript
Java必备基础知识点(超全)
Java必备基础知识点(超全)
29674 5
|
17天前
|
存储 设计模式 算法
Java基础知识点总结3
Java基础知识点总结
40 0
|
17天前
|
设计模式 安全 Java
Java基础知识点总结1
Java基础知识点总结
27 0
|
17天前
|
存储 安全 Java
Java基础知识点总结2
Java基础知识点总结
44 0
|
17天前
|
存储 Java 数据库
java基础的知识点(一)
java基础的知识点(一)
14 0
|
11月前
|
缓存 监控 Java
【Java】Java核心要点总结:61
1. java中的线程池是如何实现的 Java 中的线程池是通过 ThreadPoolExecutor 类实现的。ThreadPoolExecutor 继承自 AbstractExecutorService,并实现了 Executor、ExecutorService 和 Future 接口,它可以为程序提供管理和控制多个工作者线程的功能,提高线程重用性并避免线程频繁创建销毁的开销。
|
7月前
|
存储 Java 容器
|
8月前
|
监控 安全 Java
Java面试题-Java核心基础
Java面试题-Java核心基础
82 2