【Java学习笔记】#02并发

一.什么是线程和进程?线程与进程的关系,区别及优缺点?

  1. 进程(Process):

    • 进程是操作系统中的一个独立执行单位,每个进程都有自己的地址空间、内存和系统资源。
    • 每个进程在运行时都有一个独立的虚拟机实例(JVM实例),也就是说,每个进程都有自己的Java堆、方法区等。
    • 进程之间相互独立,一个进程的崩溃不会影响其他进程。
    • 进程间通信较为复杂,通常需要使用进程间通信(IPC)机制来实现数据传递。
  2. 线程(Thread):

    • 线程是进程中的一个执行路径,每个进程可以包含多个线程。所有线程共享进程的资源,如内存空间和文件句柄。
    • 在JVM中,一个Java应用程序运行时,实际上是一个Java进程,JVM会为每个Java线程创建一个对应的本地操作系统线程。
    • 多线程的设计可以提高程序的并发性和响应性,例如在GUI应用程序中,可以使用一个线程负责用户界面响应,另一个线程负责执行耗时的任务,避免阻塞用户界面。
  3. 线程与进程的关系:

    • 一个进程可以包含多个线程,这些线程共享该进程的资源,如堆、方法区和静态变量。
    • 所有线程都运行在同一个进程的上下文中,共享相同的地址空间和文件描述符等。
    • 因为线程共享资源,因此线程之间的通信相对更容易和高效。
  4. 线程与进程的区别:

    • 进程是一个独立的执行单位,而线程是一个进程内的执行路径。
    • 进程之间相互独立,而线程共享进程的资源。
    • 创建、撤销和切换进程比线程开销大,因为每个进程都有独立的虚拟地址空间和资源。
    • 进程间通信较为复杂,线程间通信相对容易。
  5. 优缺点:

    • 进程的优点:相对独立,一个进程的崩溃不会影响其他进程;进程间通信较为安全。
    • 进程的缺点:创建、撤销和切换进程开销较大,占用系统资源较多。
    • 线程的优点:创建、撤销和切换线程开销较小,共享资源,通信较为高效。
    • 线程的缺点:一个线程的错误可能导致整个进程的崩溃,线程间通信较为复杂,需要考虑同步和竞态条件的问题。

在Java中,由于每个线程对应一个本地操作系统线程,过多的线程可能导致系统开销增大,因此需要合理地使用线程来获得较好的性能和资源利用。

二.为什么要使⽤多线程呢?

  1. 充分利用多核 CPU 的能力:

    • 现代计算机通常都有多核 CPU,每个核心都可以独立地执行指令。单线程应用程序只能在一个核心上运行,而多线程应用程序可以将任务分解成多个线程,使得每个线程都在不同的核心上并行执行。
    • 通过使用多线程,可以充分利用多核 CPU 的能力,使得应用程序在多个核心上同时执行,从而加快任务的处理速度,提高系统的响应性和吞吐量。
  2. 提升系统的性能:

    • 在某些情况下,应用程序需要处理大量的任务,而这些任务之间可能存在一些相互独立的部分。使用单线程处理这些任务可能会导致较长的处理时间,从而影响系统的性能。
    • 通过使用多线程,可以将任务分解成多个子任务,并让每个子任务在一个独立的线程中执行。这样可以并行处理多个任务,加快整体处理速度,提升系统的性能。

需要注意的是,多线程并不是适用于所有情况的解决方案,它也带来了一些潜在的问题和挑战,例如线程之间的同步和竞态条件问题,以及可能导致资源竞争和死锁等。因此,在使用多线程时,需要谨慎考虑线程安全和性能优化,以确保其能够真正提升系统的性能。

总结起来,通过充分利用多核 CPU 的能力,以及将任务并行处理,多线程可以帮助我们提高应用程序的执行效率和系统的整体性能。然而,它需要谨慎设计和实施,以避免潜在的问题,并确保线程安全性。

三.说说线程的⽣命周期和状态?

  1. NEW(新建):

    • 当创建一个新的Thread对象时,线程处于NEW状态。
    • 在这个阶段,线程已经被创建,但还没有开始执行,尚未分配CPU资源。
  2. RUNNABLE(可运行):

    • 一旦调用了线程的start()方法,线程进入RUNNABLE状态。
    • 在RUNNABLE状态下,线程正在执行或等待执行,它可能正在等待CPU时间片或其他资源。
  3. BLOCKED(阻塞):

    • 线程在执行过程中,可能因为某些原因被阻塞而进入BLOCKED状态。
    • 当线程试图获得一个已经被其他线程持有的锁时,它会被阻塞,直到获取到锁资源为止。
  4. WAITING(等待):

    • 线程在以下情况下进入WAITING状态:
      • 调用Object.wait()方法,使线程等待某个特定条件的发生。
      • 调用Thread.join()方法,等待该线程执行完成。
      • 调用LockSupport.park()方法,暂停线程执行。
  5. TIME_WAITING(计时等待):

    • 线程在以下情况下进入TIME_WAITING状态:
      • 调用带有超时参数的Thread.sleep()方法,使线程休眠一段时间。
      • 调用带有超时参数的Object.wait()方法,使线程等待一段时间。
      • 调用带有超时参数的Thread.join()方法,等待该线程执行完成,但有一个最大等待时间。
  6. TERMINATED(终止):

    • 线程在以下情况下进入TERMINATED状态:
      • 线程的run()方法执行完毕,线程正常结束。
      • 线程执行过程中抛出了未捕获的异常,导致线程意外终止。

拓展:在操作系统中层⾯线程有 READY 和 RUNNING 状态,⽽在 JVM 层⾯只能看到 RUNNABLE 状态。

线程的状态会随着线程的执行和外部条件的变化而变化。线程状态的管理由Java虚拟机(JVM)负责,开发者可以通过合适的同步和线程控制来管理线程的状态转换,以实现多线程编程的需求。

四.什么是线程死锁?如何避免死锁?如何预防和避免线程死锁?

线程死锁是指两个或多个线程在相互持有对方需要的资源时,由于互相等待而陷入无法继续执行的状态。简而言之,就是多个线程相互等待对方释放资源,导致所有线程都无法继续执行,从而形成死锁。

为了更好地理解线程死锁,让我们通过一个简单的代码示例来演示:

public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1...");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 1: Waiting for resource 2...");
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 and resource 2...");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2...");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 2: Waiting for resource 1...");
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 2 and resource 1...");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上面的代码中,我们创建了两个线程thread1thread2,它们分别持有resource1resource2资源,并试图获取对方持有的资源。由于两个线程相互等待对方释放资源,程序将会发生死锁,无法继续执行。

为了避免死锁,可以采取以下措施:

  1. 加锁顺序: 确保线程在获取多个资源时按照相同的顺序进行加锁。这样可以避免不同线程在获取资源时出现交叉死锁的情况。

  2. 使用tryLock(): 使用tryLock()方法来尝试获取锁,如果获取失败,则可以执行其他操作,避免一直等待。

  3. 设置获取锁的超时时间: 在获取锁的过程中设置超时时间,如果在规定时间内没有获取到锁,可以放弃或重新尝试。

  4. 使用Lock对象: 使用java.util.concurrent.locks.Lock接口提供的lock()unlock()方法,确保锁的释放不受异常情况的影响。

  5. 减少锁粒度: 尽量减少锁的使用范围,避免在不必要的情况下持有锁,从而降低死锁的可能性。

在多线程编程中,预防和避免线程死锁是一项非常重要的任务。合理设计锁的使用和并发控制策略,以及充分理解多线程之间的相互影响,可以有效地避免线程死锁的发生。

五.线上项⽬遇到死锁问题该如何排查和解决?

  1. 监控和日志: 首先,通过监控系统和查看日志来确认是否真的发生了死锁。在日志中查找异常或错误信息,可以快速定位死锁的线程或资源。

  2. 线程转储和堆栈跟踪: 获取死锁发生时的线程转储(Thread Dump)和堆栈跟踪,可以帮助定位哪些线程在等待哪些资源,以及造成死锁的具体原因。

  3. 使用工具: 可以使用一些工具来帮助检测和排查死锁,如VisualVM、JConsole、jstack等。这些工具可以显示线程状态、资源锁定情况和堆栈信息。

  4. 分析死锁情况: 通过分析线程转储和堆栈信息,找出造成死锁的原因。可能是多个线程竞争相同的锁资源,并且顺序不一致导致了死锁。

  5. 加锁顺序: 确认代码中锁资源的获取顺序,是否存在不同线程获取锁的顺序不一致的情况。尽量保持所有线程以相同的顺序获取锁,避免交叉死锁的情况。

  6. 锁的释放: 确认在代码中锁的释放是否得当。确保在获取锁的情况下,及时释放锁资源,避免死锁的发生。

  7. 锁粒度: 考虑减小锁的粒度,尽量缩小同步块的范围,以减少锁竞争的机会。

  8. 使用Lock接口: 使用java.util.concurrent.locks.Lock接口提供的lock()unlock()方法来代替synchronized关键字,确保锁的释放不受异常情况的影响。

  9. 避免长时间持有锁: 在同步块中不要进行耗时的操作,尽量保持同步块的执行时间短暂,以减少锁竞争的时间。

  10. 设置获取锁的超时时间: 在获取锁的过程中设置超时时间,如果在规定时间内没有获取到锁,可以放弃或重新尝试。

如果发现死锁问题,并根据上述排查方法定位到具体的原因,就可以针对性地进行解决。在解决死锁问题时,需谨慎操作,避免引入新的问题。最好在开发和测试阶段就对代码进行合理的并发测试和检查,以确保线上项目运行时能够尽量避免死锁问题的发生。

六.synchronized 关键字

synchronized 关键字的作用:

synchronized 是 Java 中用于实现线程安全的关键字。它可以用于修饰方法和代码块,确保同一时刻只有一个线程可以访问被修饰的代码段,从而避免多线程并发访问造成的数据竞争和不一致性。

自己如何使用 synchronized 关键字:

可以在方法的声明中使用synchronized关键字来修饰整个方法,也可以在代码块中使用synchronized来修饰一段特定的代码。使用synchronized的目的是为了保证多个线程之间对共享资源的安全访问。

synchronized 关键字的底层原理:

在底层,synchronized关键字的实现涉及对象的监视器锁(Monitor)。每个 Java 对象都有一个与之关联的监视器锁,当一个线程访问一个synchronized方法或代码块时,它会尝试获取该对象的监视器锁,如果锁没有被其他线程占用,该线程就会获取锁并进入临界区,执行synchronized代码;如果锁被其他线程占用,该线程就会被阻塞,直到获取到锁。

JDK1.6 之后的 synchronized 优化:

在 JDK1.6 之后,针对synchronized进行了多项优化,主要包括偏向锁、轻量级锁和重量级锁三种状态。这些优化目的在于提高synchronized的性能,降低获取锁和释放锁的开销。

synchronized 锁升级流程:

  1. 偏向锁:在对象刚被创建时,它的锁状态为偏向锁。如果有另一个线程尝试获取这个锁,偏向锁会升级为轻量级锁。
  2. 轻量级锁:当有多个线程竞争锁时,偏向锁会升级为轻量级锁。轻量级锁使用 CAS(比较并交换)操作,尝试获取锁。如果 CAS 操作成功,线程就进入临界区,执行同步代码;否则,轻量级锁膨胀为重量级锁。
  3. 重量级锁:如果轻量级锁获取锁的操作失败,就会升级为重量级锁。重量级锁会让等待的线程进入阻塞状态,直到持有锁的线程释放锁。

synchronized 和 ReentrantLock 的区别:

  • synchronized 是 Java 内置的关键字,而 ReentrantLock 是 JDK 提供的一个类,用于实现显式锁。
  • synchronized 不需要手动释放锁,当代码块执行完毕或发生异常时,锁会自动释放;而 ReentrantLock 需要手动调用unlock()方法来释放锁。
  • synchronized 是可重入锁,同一线程在获得锁后可以再次获取,不会造成死锁;ReentrantLock也是可重入锁。
  • ReentrantLock提供了更多的功能,例如等待可中断、公平锁等特性,相比之下,synchronized使用起来更为简单。

synchronized 和 volatile 的区别:

区别二:操作性能

区别三:解决问题

区别四:使用场景

总结:

  • 区别一:用途

  • synchronized关键字用于实现线程之间的互斥访问,即同一时刻只有一个线程可以进入synchronized代码块或方法,保证数据的一致性,避免多线程并发访问造成的数据竞争和不一致性。

  • volatile关键字用于保证线程之间的可见性,即一个线程修改了共享变量的值,其他线程能够立即看到最新值,避免了由于多线程缓存不一致而导致的数据读写问题。

  • synchronized是重量级锁,涉及锁的获取和释放过程,需要涉及上下文切换和内核态操作,性能开销较大。因此,它适合用于较为复杂的同步场景。

  • volatile是轻量级的同步机制,不涉及锁的获取和释放,性能开销较小。但它不能解决多线程并发访问带来的数据竞争问题,只能保证可见性。因此,它适用于一些简单的状态标记或信号量的控制。

  • synchronized关键字可以同时保证可见性和原子性,即在synchronized代码块内部,所有操作都是原子的,不会被其他线程中断。

  • volatile关键字只能保证可见性,不能保证原子性。它仅能解决变量的可见性问题,但无法解决多线程并发访问时的原子性问题。

  • 在需要保证数据的一致性、避免多线程并发访问造成的数据竞争时,使用synchronized关键字。

  • 在需要保证共享变量的可见性,但不涉及复杂的操作时,使用volatile关键字。

  • synchronized关键字适用于复杂的同步场景,提供了更多的功能,同时保证了可见性和原子性,但性能开销较大。

  • volatile关键字适用于简单的状态标记或信号量的控制,仅保证了可见性,性能开销较小,但无法解决复杂的同步问题。

七.并发编程的三个重要特性

  1. 原子性(Atomicity): 原子性指的是一个操作或一系列操作是不可分割的,要么全部执行成功,要么全部不执行,中间不能被打断。在多线程并发环境下,保证操作的原子性是防止数据竞争和并发问题的关键。例如,一个简单的赋值操作就是原子性的,但是涉及多个步骤的操作就可能不是原子性的。为了保证原子性,我们可以使用synchronized关键字或java.util.concurrent.atomic包中提供的原子类。

  2. 可见性(Visibility): 可见性指的是当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值,而不是使用自己线程缓存中的旧值。在多核处理器系统中,每个线程都有自己的缓存,如果不保证可见性,就可能出现一个线程修改了变量的值,但其他线程仍然读取旧值的情况。为了保证可见性,可以使用synchronized关键字或volatile关键字。

  3. 有序性(Ordering): 有序性指的是程序执行的顺序按照我们期望的顺序进行,不会因为编译器、CPU优化或指令重排而打乱原有的程序顺序。在并发编程中,由于线程的交替执行和指令重排等因素,可能导致代码执行的顺序与预期不符,从而产生难以察觉的bug。为了保证有序性,可以使用synchronized关键字或java.util.concurrent包中提供的同步工具,同时在需要的地方使用volatile关键字来禁止指令重排。

八.JMM (Java Memory Model,Java 内存模型)和 happens-before 原则。

JMM(Java Memory Model,Java 内存模型)是Java中用于定义多线程并发访问共享内存的规范。它规定了线程如何与主内存和工作内存进行交互,以及如何保证多线程程序在并发访问时的正确性和一致性。JMM的目标是提供一种形式化的规范,使得程序员在编写多线程程序时可以更容易地理解和预测线程之间的行为。

在JMM中,有一个重要的概念叫做“happens-before(先行发生)”原则。happens-before原则是用来描述在多线程环境下,对共享变量的写操作与读操作之间建立关系的规则。

具体来说,如果一个操作“happens-before”另一个操作,那么第一个操作的结果对于第二个操作是可见的。这个原则有以下几个规则:

  1. 程序顺序规则(Program Order Rule): 在同一个线程中,按照程序代码的顺序,前面的操作happens-before于后续的操作。

  2. 监视器锁规则(Monitor Lock Rule): 对一个锁的解锁happens-before于后续对这个锁的加锁。

  3. volatile变量规则(Volatile Variable Rule): 对一个volatile变量的写操作happens-before于后续对这个volatile变量的读操作。

  4. 传递性(Transitive): 如果操作A happens-before于操作B,操作B happens-before于操作C,那么可以得出操作A happens-before于操作C。

通过这些happens-before规则,JMM保证了在正确使用同步机制(如synchronized关键字或volatile关键字)的情况下,多线程程序的执行结果是可预测的,不会出现意外的结果。

请注意,happens-before原则是针对正确使用同步机制的情况,如果在程序中违反了JMM的规则,就可能导致数据竞争和并发问题。因此,在编写多线程程序时,遵循JMM的规则和使用正确的同步机制非常重要。

九.volatile 关键字

volatile是Java中用于修饰变量的关键字,它具有两个主要特性:可见性和禁止指令重排。在并发编程中,volatile关键字的作用是确保多个线程对该变量的操作都能够正确地感知到最新值,避免出现可见性问题和指令重排带来的不一致性。

1. 可见性: volatile关键字保证了变量的可见性。在多线程环境下,当一个线程修改了volatile修饰的共享变量的值,其他线程能够立即看到这个最新的值,而不是使用自己线程缓存中的旧值。这是因为在JMM中,对一个volatile变量的写操作happens-before于后续对该变量的读操作,确保了变量值的更新对于其他线程是可见的。

2. 禁止指令重排: volatile关键字禁止了指令重排优化。在编译器和处理器对代码进行优化时,可能会将多个指令进行重排,以提高性能。但在多线程环境下,这种重排可能会导致结果不一致的问题。通过使用volatile关键字,可以防止指令重排,保证了程序的正确执行顺序。

综合上述特性,volatile关键字在并发编程中的使用场景如下:

  • 当一个共享变量被多个线程访问并且其中有一个线程修改了该变量的值时,应该使用volatile关键字来保证变量的可见性,以便其他线程能够立即感知到变量的最新值。

  • 当某个变量的值不依赖于当前值,或者该变量不与其他状态变量共同参与不变约束时,可以使用volatile关键字来代替synchronized关键字,从而避免使用锁带来的性能开销。

需要注意的是,尽管volatile关键字提供了可见性和禁止指令重排的特性,但它无法保证复合操作的原子性。如果一个变量的操作不是原子的,仍然需要使用synchronized关键字或java.util.concurrent.atomic包中提供的原子类来保证原子性。

十.ThreadLocal 关键字

ThreadLocal是Java中的一个关键字,用于在多线程环境下实现线程的局部变量。它允许每个线程都有自己独立的变量副本,从而避免了多线程之间共享变量带来的线程安全问题。

底层原理: ThreadLocal通过在每个线程内部创建一个独立的副本来实现线程的局部变量。具体来说,ThreadLocal对象在内部维护一个Map,其中键为线程对象,值为线程的局部变量副本。每个线程访问ThreadLocal对象时,都会获取到该线程对应的局部变量副本,从而实现了线程隔离。

内存泄露问题: 使用ThreadLocal时需要注意内存泄漏问题。因为ThreadLocal的局部变量副本是与线程相关的,如果没有正确地进行处理,在线程结束后,局部变量的引用可能无法被回收,从而导致内存泄漏。

为避免内存泄漏,应该及时清理线程的局部变量。一般来说,可以通过在ThreadLocal使用完毕后调用remove()方法来清理线程的局部变量。另外,使用ThreadLocal时,应该在合适的时机清理掉线程局部变量,例如在请求结束时、线程池中线程的生命周期结束时等。

在项目中使用 ThreadLocal 关键字: ThreadLocal在项目中的应用场景比较广泛,尤其在Web应用中的线程池场景下常被使用。常见的使用场景包括:

  1. 存储用户信息: 在Web应用中,可以使用ThreadLocal存储当前用户的信息,以避免在方法参数中频繁传递用户信息。

  2. 数据库连接管理: 在数据库连接池中,可以使用ThreadLocal来管理每个线程所使用的数据库连接,保证每个线程都拥有独立的数据库连接。

  3. 国际化(i18n): 在多语言国际化的场景下,可以使用ThreadLocal存储当前线程的语言环境信息。

  4. 性能优化: 在性能优化中,可以使用ThreadLocal缓存一些中间结果,避免重复计算。

在使用ThreadLocal时,要确保正确地清理线程局部变量,以避免内存泄漏问题。另外,需要注意ThreadLocal并不是万能的解决方案,它适用于线程隔离的场景,但并不能替代其他线程同步机制,如synchronized等。正确使用ThreadLocal可以简化多线程编程,但同时也要注意潜在的内存泄漏问题。

十一.线程池

线程池是一种用于管理线程的机制,它可以有效地重用线程,避免频繁地创建和销毁线程,从而提高系统的性能和资源利用率。Java中的线程池是通过java.util.concurrent包提供的。在Java中,有几种不同类型的线程池,每种线程池都有其优缺点和适用场景。

1. 固定大小线程池(FixedThreadPool): 固定大小线程池中的线程数量是固定的,一旦线程池创建完成,线程池中的线程数将不再发生变化。当线程池中的线程处于空闲状态时,它们仍然会保持活动状态,可以立即响应任务的执行请求。优点是可以控制线程数量,避免线程的创建和销毁开销,适用于稳定且长期运行的任务。

2. 缓存线程池(CachedThreadPool): 缓存线程池的线程数量是根据需要动态调整的,当有新任务提交时,如果线程池中有空闲线程,则直接使用空闲线程来执行任务;如果线程池中没有空闲线程,则创建新线程来执行任务。优点是可以根据实际任务的数量动态调整线程数,适用于短期的异步任务。

3. 单线程池(SingleThreadExecutor): 单线程池只有一个线程,它会顺序地执行所有任务。如果该线程因异常退出,会重新创建一个新的线程来替代。优点是保证任务的顺序执行,适用于需要顺序执行任务的场景。

4. 定时线程池(ScheduledThreadPool): 定时线程池用于延时执行任务或定时执行任务。可以指定任务在延时一定时间后执行,或者定期地执行任务。适用于需要定时或周期性执行任务的场景。

线程池的重要参数:

  • 核心线程数(corePoolSize):线程池的基本大小,即线程池中保持活动的最小线程数量。
  • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。
  • 空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程会在一定时间内等待新任务的到来,超过这个时间就会被回收。
  • 任务队列(workQueue):用于存储等待执行的任务的队列。
  • 线程工厂(threadFactory):用于创建线程的工厂。

线程池的执行流程:

  1. 线程池收到一个任务。
  2. 如果线程池中的线程数量小于核心线程数,直接创建新的线程执行任务。
  3. 如果线程池中的线程数量大于等于核心线程数,将任务放入任务队列。
  4. 如果任务队列已满,且线程池中的线程数量小于最大线程数,则创建新的线程执行任务。
  5. 如果线程池中的线程数量大于等于最大线程数,执行饱和策略来处理新的任务。

线程池的饱和策略:

  • AbortPolicy(默认):直接抛出异常,阻止系统正常运行。
  • CallerRunsPolicy:使用当前调用线程来执行任务。
  • DiscardOldestPolicy:抛弃任务队列中最旧的任务,然后尝试执行新的任务。
  • DiscardPolicy:直接抛弃新的任务,不做任何处理。

如何设置线程池的大小: 设置线程池的大小要根据具体的业务场景来决定。一般来说,可以根据以下几个因素来选择合适的线程池大小:

  • 任务的类型和特性:如果任务是计算密集型的,建议使用固定大小线程池;如果是I/O密集型的,可以考虑使用缓存线程池。
  • 系统资源:根据服务器的CPU核数、内存大小等系统资源来决定线程池的最大线程数。
  • 预估负载:根据预估的任务负载来确定线程池的大小,避免线程池过大或过小。

正确设置线程池的大小可以充分利用系统资源,提高系统的性能和响应速度。但是,过度使用线程池也可能导致资源浪费和系统的压力增加,因此需要根据具体场景进行权衡和调优。

十二.ReentrantLock 和 AQS

ReentrantLock是Java中提供的可重入锁,它比传统的synchronized关键字提供了更多的灵活性和功能。ReentrantLock实现了Lock接口,可以通过显式调用lock()unlock()方法来获取和释放锁。

ReentrantLock的特性:

  1. 可重入性:同一个线程可以多次获取同一个ReentrantLock,而不会发生死锁。
  2. 公平性:ReentrantLock可以设置为公平锁,在多个线程竞争锁时按照先来先得的顺序获取锁,避免线程饥饿问题。
  3. 中断响应:ReentrantLock支持对线程的中断响应,在等待锁的过程中,如果其他线程中断了当前线程,它会立即响应中断。
  4. 条件变量:ReentrantLock提供了Condition接口,可以通过条件变量实现线程的等待和通知机制。

ReentrantLock的实现是基于AQS(AbstractQueuedSynchronizer),AQS是Java中实现锁和同步器的框架。AQS使用一个FIFO的双向队列来管理等待获取锁的线程,它的内部维护了一个volatile的int类型变量state来表示锁的状态,state的具体含义由子类来定义。

ReentrantLock通过继承AQS并实现其中的几个方法来实现锁的功能。主要包括:

  1. tryAcquire(int arg):尝试获取锁,如果成功获取返回true,否则返回false。
  2. tryRelease(int arg):尝试释放锁,如果成功释放返回true,否则返回false。
  3. tryAcquireShared(int arg):尝试获取共享锁,如果成功获取返回非负数,否则返回负数。
  4. tryReleaseShared(int arg):尝试释放共享锁,如果成功释放返回true,否则返回false。

ReentrantLock还通过ConditionObject类实现了Condition接口,用于实现条件变量。条件变量可以实现线程的等待和通知机制,通过await()方法让线程等待某个条件,通过signal()方法通知等待中的线程继续执行。

总的来说,ReentrantLock是基于AQS的可重入锁实现,通过AQS提供的框架,ReentrantLock实现了锁的特性,包括可重入性、公平性、中断响应和条件变量等功能。它提供了更多的灵活性和功能,使得在复杂的同步场景中可以更加方便地控制和管理锁。

十三.乐观锁和悲观锁的区别

乐观锁和悲观锁是两种不同的并发控制机制,用于解决多线程并发访问共享资源时的数据竞争和一致性问题。

悲观锁: 悲观锁假设在整个数据操作过程中会有其他线程对数据进行修改,因此在进行操作前会先锁定资源,确保其他线程不能同时修改数据。悲观锁使用的是排他锁,一旦一个线程获得锁,其他线程将被阻塞,直到该线程释放锁。

悲观锁的特点:

  • 适用于写操作较多的场景,避免多个线程同时写入导致的数据不一致问题。
  • 需要频繁加锁和释放锁,降低了并发性能。
  • 可能会导致死锁问题,如果某个线程获取了锁,但无法释放锁(例如发生异常),其他线程就会一直等待。

乐观锁: 乐观锁假设在整个数据操作过程中不会有其他线程对数据进行修改,因此在进行操作前不会加锁,而是在更新数据时检查数据是否被其他线程修改过。乐观锁使用的是版本号、时间戳等机制来检查数据的一致性。

乐观锁的特点:

  • 适用于读操作较多、冲突较少的场景,提高了并发性能。
  • 不需要加锁,减少了锁的开销。
  • 如果检查发现数据已经被其他线程修改,需要回滚操作并重试,增加了代码复杂性。

乐观锁和悲观锁的选择取决于具体的业务场景和数据访问模式。悲观锁在写操作较多,且有较多的数据冲突时更为适用,但可能降低并发性能。乐观锁在读操作较多,且数据冲突较少时更为适用,但需要考虑并发冲突的处理。在实际应用中,可以根据业务需求和性能要求来选择合适的锁机制。

十四.CAS原理?什么是 ABA 问题?ABA 问题怎么解决?

CAS(Compare and Swap)是一种乐观锁的实现方式,用于解决多线程并发访问共享资源时的原子性问题。CAS操作包含三个操作数:内存位置(V)、期望的值(A)和新的值(B)。如果内存位置的值等于期望的值,就将该位置的值更新为新的值,否则不做任何操作。

CAS的原理:

  1. 读取内存位置的当前值V。
  2. 检查当前值V是否等于期望的值A,如果相等则执行第4步,否则执行第3步。
  3. 重新读取内存位置的当前值,重复步骤2。
  4. 将内存位置的值更新为新的值B。

CAS操作是一个原子操作,它不需要加锁,可以避免了悲观锁的性能开销。在并发情况下,如果多个线程同时执行CAS操作,只有一个线程的CAS操作会成功,其他线程的CAS操作会失败,失败的线程可以根据需要进行重试或采取其他处理方式。

ABA问题: ABA问题是指在CAS操作中,如果内存位置的值由A变为B,然后再由B变为A,这个过程中可能导致某些线程错误地认为内存位置的值没有发生变化,从而造成数据不一致的问题。虽然内存位置的值在两次CAS操作之间确实没有发生变化,但实际上经历了一次变化过程。

解决ABA问题: 为了解决ABA问题,可以采用版本号、时间戳等方式引入额外的变量,每次对内存位置的值进行更新时,都更新版本号或时间戳,这样即使值相同,但版本号或时间戳不同,也能正确识别出数据的变化。

Java中的java.util.concurrent.atomic包中的原子类,如AtomicIntegerAtomicLong等,就是通过使用volatile+CAS的方式来解决ABA问题。它们在进行CAS操作时,会同时检查版本号或时间戳,从而避免了ABA问题。此外,Java中的AtomicStampedReference类专门用于解决ABA问题,它可以将值与版本号组合在一起,从而在CAS操作中判断值和版本号是否都相等,从而避免了ABA问题。

总的来说,CAS是一种乐观锁的实现方式,通过对比内存位置的值来判断数据是否被修改过,从而保证原子性。ABA问题是CAS操作的一个潜在问题,可以通过引入版本号或时间戳等方式来解决。在Java中,很多并发工具和数据结构都采用了CAS机制来保证并发安全,如ConcurrentHashMap和Atomic原子类。

十五.Atomic 原⼦类

Atomic原子类是Java中java.util.concurrent.atomic包下的一组用于实现原子操作的类。这些类提供了一些常见的基本数据类型的原子操作,如AtomicIntegerAtomicLongAtomicBoolean等,以及AtomicReferenceAtomicStampedReference等用于引用类型的原子操作。

原子操作是不可分割的单个操作,不会被其他线程中断,从而保证了操作的完整性和一致性。Atomic原子类利用底层的CAS(Compare and Swap)操作,通过volatile关键字来实现原子性。CAS操作可以保证在多线程环境下,只有一个线程能够成功执行操作,其他线程的操作将会失败,从而避免了线程安全问题。

一些常见的Atomic原子类:

  1. AtomicInteger:用于对int类型进行原子操作。
  2. AtomicLong:用于对long类型进行原子操作。
  3. AtomicBoolean:用于对boolean类型进行原子操作。
  4. AtomicReference:用于对引用类型进行原子操作。
  5. AtomicStampedReference:用于解决ABA问题,它可以将值与版本号组合在一起,从而在CAS操作中判断值和版本号是否都相等,避免了ABA问题。

这些Atomic原子类的方法提供了一系列的原子操作,例如get()set()compareAndSet()getAndIncrement()getAndSet()等等。通过这些原子操作,可以实现对共享变量的线程安全操作,而无需显式使用锁,从而提高了多线程并发访问时的性能和效率。

在编写多线程程序时,如果需要进行简单的原子操作,可以优先考虑使用Atomic原子类,而不是显式地使用synchronized关键字或ReentrantLock,从而简化代码并提高并发性能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、锁、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少锁竞争、避免死锁等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值