多线程之并发变成高级面试题-2024

有三个线程T1,T2,T3,如何保证顺序执行

  • 使用 join() 方法: 可以在每个线程内部使用 join() 方法来等待前一个线程执行完成。具体操作是在线程 T2 的 run() 方法中调用 T1.join(),在线程 T3 的 run() 方法中调用 T2.join()。这样可以确保 T1 在 T2 之前执行,T2 在 T3 之前执行。


Thread T1 = new Thread(() -> {
    // 线程 T1 的任务
});

Thread T2 = new Thread(() -> {
    try {
        T1.join(); // 等待 T1 执行完成
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 线程 T2 的任务
});

Thread T3 = new Thread(() -> {
    try {
        T2.join(); // 等待 T2 执行完成
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 线程 T3 的任务
});

T1.start();
T2.start();
T3.start();

  • 使用 CountDownLatch: 可以使用 CountDownLatch 来控制线程的执行顺序。创建一个 CountDownLatch 对象,设置初始计数为 2,分别在 T1 和 T2 的线程内等待计数器减少到 0,然后释放 T3 线程。

CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("T1 running.");  // t1 运行
    latch1.countDown(); // T1 执行完后释放 latch1
});

Thread t2 = new Thread(() -> {
    try {
        latch1.await(); // 等待 latch1 的释放
          System.out.println("T2 running.");  // t2运行
        latch2.countDown(); // T2 执行完后释放 latch2
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

Thread t3 = new Thread(() -> {
    try {
        latch2.await(); // 等待 latch2 的释放
          System.out.println("T3 running.");  // t3 运行
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

t1.start();
t2.start();
t3.start();

说说进程和线程的区别

        当一个程序在计算机上运行时,通常会创建至少一个进程。进程被认为是操作系统分配资源的最小单元每个进程都拥有独立的内存空间和系统资源,包括文件句柄和网络连接等。操作系统通常使用进程来表示独立的应用程序实例。比如,你的计算机上可能同时运行着浏览器、文本编辑器、音乐播放器等多个进程。

        每个进程至少包含一个线程,通常被称为主线程。线程被视为操作系统调度的最小单元,它们共享相同的进程内存空间和系统资源。在一个进程内,多个线程可以协同工作,执行不同的任务,共享数据。这种多线程的使用方式有助于提高程序的并发性和性能。例如,一个文字处理软件的进程可能包括一个主线程,用于处理用户界面响应,同时还有一个后台线程,负责自动保存文件。

Java线程之间是如何通信的

        共享内存: 这是一种常见的方式,多个线程可以访问同一个共享内存区域,通过读取和写入共享内存中的数据来进行通信和同步。在Java中,我们可以使用共享变量或共享数据结构来实现共享内存通信。例如,可以使用 volatile 关键字来确保共享变量的可见性,以及使用等待和通知机制,即 wait()notify() 方法,来实现线程之间的协作。这种方式适用于需要高效共享数据的场景,但需要谨慎处理数据竞争和同步问题。

        消息传递: 另一种方式是消息传递,多个线程之间通过消息队列、管道、信号量等机制来传递信息和同步状态。这种方式通常涉及线程之间的显式消息发送和接收操作,使线程能够协调它们的工作。例如,我们可以使用信号量机制,通过获取和释放许可证来控制线程的访问。又或者使用栅栏机制,通过等待所有线程达到栅栏点来同步它们的执行。此外,锁机制也是一种重要的消息传递方式,通过获取和释放锁来实现线程之间的互斥和同步。消息传递的优点在于可以实现更松散的耦合,线程之间不需要直接共享内存,从而减少了潜在的竞争条

说说synchronized与ReentrantLock的区别

  • 用法不同:synchronized 可以用于修饰普通方法、静态方法以及代码块,而 ReentrantLock 仅适用于代码块。
  • 获取锁和释放锁方式不同:Synchronized 是隐式锁,可以自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。ReentrantLock 是显式锁,需要手动加锁和释放锁, 在使用之前需要先创建 ReentrantLock 对象,然后使用 lock 方法进行加锁,使用完之后再调用 unlock 方法释放锁。
  • 锁类型:默认情况下,synchronized 是非公平锁,而 ReentrantLock 也是非公平锁,但可以手动将 ReentrantLock 配置为公平锁,允许线程按照它们请求锁的顺序获取锁。
  • 中断响应: synchronized 无法直接响应中断,可能导致线程在锁上无限期地等待。ReentrantLock 具有响应中断的能力,可以在等待锁的过程中响应线程的中断请求,从而避免潜在的死锁情况。
  • 底层实现:synchronized 是一个关键字,是在JVM层面通过监视器实现的,而 ReentrantLock 是基于AQS实现的。

ThreadLocaL如何防止内存泄漏

  1. 使用完 ThreadLocal 后及时调用 remove() 方法:在不再需要使用 ThreadLocal 存储的数据时,手动调用 ThreadLocal.remove() 方法将该数据从当前线程的 ThreadLocalMap 中清除。这样可以确保 ThreadLocalMap 不会持有对对象的引用,从而帮助垃圾回收器正常回收不再需要的对象。

说说ThreadLocal原理

        ThreadLocal 是 Java 中的一个类,用于实现线程封闭(Thread-local)的数据存储机制。每个线程都有自己的 ThreadLocalMap,其中 ThreadLocal 对象充当键,线程的变量副本作为对应键的值。使用 ThreadLocal 的 set() 方法将数据存储在当前线程的 ThreadLocalMap 中,而使用 get() 方法则是从当前线程的 ThreadLocalMap 中获取数据副本。

        这种机制为每个线程维护独立的变量副本,这样可以实现线程之间的数据隔离,从而有效地避免了线程安全问题。这对于需要在线程内部存储线程特定数据的情况非常有用,例如数据库连接、用户会话信息等。但需要谨慎使用 ThreadLocal,确保在不再需要时及时清理变量副本,以避免潜在的内存泄漏问题。这种机制提供了一种有效的方式来确保线程级别的数据隔离和线程安全性。        

线程池中核心线程数量大小怎么设置

CPU密集型任务比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

如何确保线程安全

  1. 使用synchronized关键字:synchronized关键字可以确保同一时刻只有一个线程可以执行某个代码块,从而避免了多个线程同时访问和修改共享资源的问题。
  2. 使用Atomic类:Java提供了多个原子类,如AtomicInteger、AtomicLong等,它们可以保证对基本数据类型的原子性操作,避免了使用synchronized关键字和volatile关键字的限制。
  3. 使用ReentrantLock类:ReentrantLock类是Java提供的一种可重入锁,与synchronized关键字类似,但它提供了更多的灵活性和功能。
  4. 使用线程安全的数据结构:Java提供了多种线程安全的数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等,这些数据结构内部已经实现了线程安全,可以直接使用。
  5. 使用线程池:线程池可以避免创建和销毁线程的开销,并且可以有效地控制并发量,保证线程安全。
  6. 避免共享状态: 如果可能,尽量避免多个线程共享状态。将数据封装在线程内部,减少共享数据的需求。
  7. 使用线程安全的设计模式: 了解并应用线程安全的设计模式,如单例模式中的双重检查锁定等。

什么是可重入锁

                可重入锁可以简单理解为一个可以重复获取的锁,就像拿钥匙开锁一样,你可以反复用同一把钥匙开锁。这种锁在同一线程内是安全的,因为它可以被同一线程多次获取,而不会产生不一致的状态。

举个例子,假设有一个线程A在执行一个方法,同时这个方法内部又调用另一个方法,那么线程A可以重复获取同一个锁,而不会出现死锁的情况。因为同一线程可以多次获取同一个锁,所以这种锁机制避免了死锁的发生。

        但是需要注意,在使用可重入锁时,必须保证在释放锁之前已经获取了该锁,否则会导致死锁。同时还需要保证在获取锁的时候没有嵌套地获取其他锁,否则也会导致死锁。另外,还必须保证在获取锁的时候没有阻塞其他线程,否则同样会导致死锁。

锁的优化机制了解吗

  1. 偏向锁(Biased Locking):偏向锁是一种针对无竞争情况的锁优化机制。它通过消除无谓的获取锁和释放锁的操作,提高了程序的性能。偏向锁会记录哪个线程正在访问某个对象,并且后续的访问请求如果是同一个线程,就可以直接访问,而不需要加锁。
  2. 轻量级锁(Lightweight Locking):轻量级锁是一种针对单线程访问的情况的锁优化机制。它通过使用标记位或者CAS操作来对共享资源进行加锁和解锁,避免了使用重量级锁时的上下文切换和内核态切换等开销。
  3. 自旋锁(Spin Lock):自旋锁是一种非阻塞的锁机制,当线程无法立即获取锁时,它会持续检查锁是否被释放,直到获取到锁为止。自旋锁可以减少线程的上下文切换开销,但在锁持有时间较长的情况下,会浪费CPU资源。
  4. 适应性自旋锁(Adaptive Spin Lock):适应性自旋锁是一种结合了自旋锁和阻塞锁的锁机制。在刚开始时,线程会采用自旋的方式来等待锁的释放,但随着时间的推移,如果锁仍然没有被释放,线程会逐渐切换到阻塞状态,从而减少CPU资源的浪费。
  5. 分段锁(Segmented Locking):分段锁是一种针对共享资源过多的情况下的锁优化机制。它将共享资源分成多个段,每个线程只需要对其中一部分进行加锁和解锁操作,从而减少了锁的竞争和开销。
  6. 乐观锁(Optimistic Locking):乐观锁是一种基于冲突检测的锁机制。它假设多个线程同时访问和修改同一个数据的概率较小,因此在读取数据时不会加锁,而是在提交修改时检测是否存在冲突。如果存在冲突,则进行回滚或重试操作。乐观锁适用于读操作较多的场景。
  7. 锁粗化(Lock Coarsening):锁粗化是一种针对长时间持有锁的场景的优化策略。如果一个线程在短时间内需要连续多次加锁和解锁,那么可以将这些加锁和解锁操作合并成一个较大的加锁和解锁操作,从而减少了加锁和解锁的次数,提高了效率。

说说你对JMM内存模型的理解

        在JMM中,内存主要划分为两种类型:主内存和工作内存。主内存存储了所有的对象实例和静态变量,而工作内存存储了每个线程的局部变量、栈中的部分区域以及寄存器的内容

        JMM定义了一系列规则来规范线程之间的内存访问其中最重要的规则是:当一个线程想要访问一个共享变量的值时,它必须先将其本地内存中的值更新到主内存中,然后再从主内存中读取该变量的值。这个过程被称为“主内存的一致性”。

        JMM的作用主要是屏蔽底层硬件和操作系统的内存访问差异,实现平台一致性,使得Java程序在不同平台下能达到一致的内存访问结果。同时,JMM也规范了JVM如何与计算机内存进行交互,从而保证并发程序的正确性和可靠性。

说下对AQS的理解

排他锁也称为独占锁,在多个线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,即多个线程中只有一个线程获得锁资源。在AQS中,排他锁是通过内置的同步状态来实现的。当同步状态为0时,表示锁是未被获取的;当同步状态大于0时,表示锁已经被获取且被占用;当同步状态小于0时,表示锁已经被获取但是处于等待状态。

共享锁允许多个线程同时获得锁资源,但是在同一时刻只有一个线程可以获取到锁的拥有权,其他线程需要等待该线程释放锁。在AQS中,共享锁的实现与排他锁类似,也是通过内置的同步状态来实现的。

AQS通过一个内置的FIFO(先进先出)等待队列来实现线程的排队和调度。当线程需要获取锁资源时,如果锁已经被其他线程获取,则该线程会被加入到等待队列中等待。当锁被释放时,等待队列中的第一个线程会获得锁资源并继续执行。

在实现AQS时,需要继承自AQS类并实现其抽象方法。其中比较重要的方法包括:tryAcquire()和tryRelease()方法,用于实现锁的获取和释放;acquire()和release()方法,用于实现阻塞和唤醒操作;isHeldExclusively()方法,用于判断是否是排他锁。

总之,AQS是Java并发编程中的重要组件之一,它提供了线程同步的底层实现机制。在使用AQS时,需要根据具体的应用场景选择合适的锁机制来实现线程的同步和互斥操作。

说下CAS的原理

CAS(Compare And Swap)是一种乐观的并发控制机制,它的核心原理是基于硬件层面的原子性保证。CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。它的工作原理是:

  1. 在将新值写入内存之前,CAS操作会先比较内存位置的值是否与预期原值相匹配。
  2. 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置的值更新为新值。
  3. 如果内存位置的值与预期原值不匹配,则CAS操作失败,不会修改内存值。

CAS的优势在于它没有阻塞状态,不会引起线程上下文的切换和调度问题。然而,CAS也存在一些缺点,例如ABA问题和开销问题。ABA问题是指一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。开销问题则是因为CAS自旋操作需要不断轮询内存位置,直到成功为止,这会消耗大量的CPU资源。

如何停止一个正在运行的线程

  1. 使用标志位: 在线程的执行体中使用一个标志位,当该标志位为true时,线程会自行退出执行。这是一种比较安全和可控的方式。例如:

public class MyThread extends Thread {
    private volatile boolean stopRequested = false;

    public void run() {
        while (!stopRequested) {
            // 执行线程任务
        }
    }

    public void stopThread() {
        stopRequested = true;
    }
}

     2、使用Thread.stop方法(不推荐使用):Thread.stop方法可以强制停止一个线程,但不建议使用它,因为它可能导致线程的状态不一致和资源泄漏等问题,容易引发不可预测的错误。

说下你对volatile的理解

  1. 保证可见性:volatile保证了多个线程对共享变量的操作是可见的。当一个线程修改了共享变量的值,其他线程会立即看到这个改变
  2. 禁止指令重排:volatile通过禁止指令重排来保证顺序性。在多线程环境下,为了提高程序执行效率,编译器和处理器可能会对指令进行重新排序。但是,如果一个变量被volatile修饰,就禁止了指令重排,确保每个线程都能看到正确的操作顺序。

线程池的底层工作原理

  1. 线程池的创建: 在使用线程池之前,需要首先创建一个线程池。通常,线程池会根据配置参数(如核心线程数、最大线程数、队列类型等)来初始化线程池的基本属性。
  2. 任务提交: 当有任务需要执行时,将任务提交给线程池。任务可以是RunnableCallable对象,表示需要在一个独立线程中执行的工作单元。
  3. 线程分配: 线程池内部维护了一组工作线程,这些线程会被动态分配来执行任务。线程池首先会尝试将任务分配给核心线程,如果核心线程数没有达到上限,就创建一个新的核心线程来执行任务。如果核心线程已满,任务会被放入任务队列中等待执行。
  4. 任务执行: 分配给线程的任务会被执行。每个工作线程会不断地从任务队列中获取任务并执行它们。一旦任务执行完成,线程可以选择等待新任务或被回收,具体取决于线程池的配置和实现方式。
  5. 线程回收: 线程池内的线程可能会被回收,这可以是根据一些策略,如闲置时间超过一定阈值或线程数超过最大线程数等。回收的线程会释放资源,如内存和CPU,以便在需要时重新使用。
  6. 任务完成和结果返回: 任务执行完成后,可以将执行结果返回给调用者。如果任务是通过Callable提交的,线程池会返回Future对象,通过该对象可以获取任务的执行结果。
  7. 异常处理: 线程池通常会处理任务执行过程中抛出的异常,可以将异常信息记录下来或采取适当的措施,以确保线程池的稳定性

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值