一文彻底搞懂JUC常见面试题

1. JUC简介

JUC(Java Util Concurrent)是Java工具包中提供的并发编程工具集,它位于java.util.concurrent包下,主要用于简化多线程编程、提高程序性能和可维护性。JUC提供了一系列线程安全的数据结构、并发工具类和线程池等。

JUC中一些常用的并发工具和类:

  • 并发集合类(Concurrent Collections): JUC提供了一系列线程安全的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet等,用于替代传统的非线程安全集合类,可以安全地在多线程环境中使用。

  • 原子变量类(Atomic Variables): JUC提供了一系列原子操作类,如AtomicInteger、AtomicLong、AtomicReference等,用于在多线程环境中执行原子操作,避免了使用锁的开销,提高了程序的性能。

  • 同步工具类(Synchronization Utilities): JUC提供了一些同步工具类,如CountDownLatch、CyclicBarrier、Semaphore等,用于在多线程之间进行协调和同步,实现线程间的等待、通知和同步操作。

  • 线程池(Executor Framework): JUC提供了一套灵活强大的线程池框架,包括Executor、ExecutorService、ThreadPoolExecutor等,用于管理和调度线程的执行,提高了程序的性能和资源利用率。

  • 并发工具类(Concurrent Utilities): JUC还提供了一些其他的并发工具类,如Lock、ReadWriteLock、Phaser、CompletableFuture等,用于实现更复杂的并发模式和解决特定的并发问题。

2. 进程和线程的区别?

进程是程序的一次执行过程,拥有独立的地址空间和资源,是操作系统进行资源分配和调度的基本单位;而线程是进程中的一个执行单元,共享进程的地址空间和资源,是操作系统进行调度的基本单位,多个线程共享同一进程的内存空间和资源。

3. 多线程和多进程的区别?

多线程是在同一进程内部并发执行多个线程,共享同一进程的地址空间和资源,而多进程是在不同进程之间并发执行多个进程,各自拥有独立的地址空间和资源。

4. 说一下并发编程中的3个概念?(原子、可见、有序)

在并发编程中,有三个重要的概念:原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)。

原子性(Atomicity)
原子性是指操作不可被中断的特性,即要么全部执行成功,要么全部不执行,不存在部分执行的情况。在并发编程中,原子操作是一系列操作,要么全部执行成功,要么全部失败,不会被其他线程中断或干扰。

可见性(Visibility)
可见性是指一个线程对共享变量的修改能够被其他线程立即感知到的特性。在并发编程中,如果一个线程修改了共享变量的值,其他线程应该能够立即看到这个变化,而不是在很长一段时间内仍然保持旧值。

有序性(Ordering)
有序性是指程序执行的顺序与代码中的顺序一致的特性。在单线程环境下,程序按照代码中的顺序依次执行;但在并发编程中,由于指令重排等原因,可能会导致程序执行顺序与代码中的顺序不一致。在并发编程中,有序性需要通过同步机制来保证,例如使用锁、volatile关键字、原子操作等。

5. Java的内存模型以及如何保证三种特性?

Java的内存模型(Java Memory Model,JMM)定义了Java程序中线程之间如何访问共享内存的规范,保证了线程安全性、原子性、可见性和有序性等特性。

线程安全性(Thread Safety)
Java内存模型通过各种同步机制(如synchronized关键字、Lock接口、volatile关键字、原子类等)来保证线程安全性。
synchronized关键字可以对代码块或方法进行加锁,确保同一时间只有一个线程可以执行被锁定的代码,从而避免了多线程并发访问共享资源时的竞态条件。
volatile关键字用于声明变量,保证了变量的可见性和有序性,但不保证原子性。
原子类(如AtomicInteger、AtomicLong等)提供了一系列的原子操作,确保了对共享变量的操作是原子的,不会被中断。

原子性(Atomicity)
Java内存模型通过原子类、synchronized关键字等机制来保证对共享变量的操作是原子的。
原子类(Atomic类)提供了一系列的原子操作,如compareAndSet、incrementAndGet、decrementAndGet等,确保了对共享变量的操作是不可分割的,要么全部成功,要么全部失败。

可见性(Visibility)
Java内存模型通过volatile关键字来保证共享变量的可见性。
当一个变量被volatile修饰时,对这个变量的写操作会立即被其他线程所看到,而不会出现缓存一致性的问题,保证了共享变量的修改对其他线程的立即可见性。

有序性(Ordering)
Java内存模型通过volatile关键字和synchronized关键字来保证有序性。
volatile关键字保证了对volatile变量的读写操作都是按照程序中的顺序进行的,不会出现重排序的情况。
synchronized关键字不仅保证了临界区内代码的有序执行,还通过内存屏障(Memory Barrier)来保证了临界区前后的内存操作不会被重排序。

6. 说一下volatile关键字?

volatile是Java中的一个关键字,用于修饰变量,其主要作用是保证变量的可见性和禁止指令重排序,但并不保证原子性。 因此,在多线程环境下,volatile适用于某个变量的写操作不依赖于当前值的情况,而且变量的写操作不会与其他变量的操作存在依赖关系。

可见性(Visibility)
当一个变量被volatile修饰时,对这个变量的修改会立即被其他线程所看到,而不会出现缓存一致性的问题。这是因为volatile变量的写操作会立即被刷新到主内存中,并且对volatile变量的读操作都是直接从主内存中读取的,而不是从线程的工作内存中读取。

禁止指令重排序(Prevent Instruction Reordering)
volatile关键字还可以防止指令重排序,即被volatile修饰的变量在读写操作时会被插入内存屏障(Memory Barrier),防止编译器和处理器对其进行重排序优化。这样可以保证变量的修改顺序和代码中的顺序一致,避免了可能导致数据不一致的重排序情况。

不保证原子性(No Atomicity)
尽管volatile可以保证变量的可见性和禁止指令重排序,但它并不保证对变量的操作是原子的。如果一个变量的操作需要保证原子性,例如自增或自减操作,需要使用synchronized关键字或java.util.concurrent.atomic包下的原子类来保证。

7. 说一下Synchronized关键字?

synchronized关键字是Java中实现同步锁机制的一种方式,通过对共享资源的加锁和释放锁来保证了线程安全,避免了多线程并发访问时可能出现的竞态条件和数据不一致问题。

实现原理:
synchronized关键字可以用来修饰方法或代码块,在多线程环境下,当一个线程执行synchronized修饰的方法或代码块时,会尝试获取对象的锁(或者类的锁),如果获取成功,则可以执行对应的代码,如果获取失败,则会被阻塞,直到获取到锁为止。
当线程执行完synchronized修饰的方法或代码块时,会释放对象的锁,其他等待的线程可以继续竞争锁。

保证原子性
synchronized关键字可以保证被修饰的方法或代码块的原子性,即同一时间只有一个线程可以执行该方法或代码块,避免了多线程并发访问共享资源时可能出现的竞态条件和数据不一致问题。
通过synchronized关键字保护的代码块,在同一时间只允许一个线程执行,其他线程需要等待前一个线程执行完毕释放锁后才能执行,从而确保了对共享资源的安全访问。

保证可见性
除了保证原子性外,synchronized关键字还可以保证共享变量的可见性。当一个线程释放锁时,会将修改的共享变量的值刷新到主内存中,而其他线程在获取锁时会从主内存中重新读取该变量的值,确保了共享变量的修改对其他线程的可见性。

锁的粒度
synchronized关键字可以修饰方法、静态方法、代码块,以及类对象,因此可以根据实际需求选择合适的锁粒度。
方法级别的同步锁适用于整个方法需要同步的情况,代码块级别的同步锁适用于部分代码需要同步的情况,静态方法和类对象级别的同步锁适用于静态资源的同步访问。

8. Java中确保线程安全的方法?(Synchronized和Lock、thradlocal和同步,悲观锁和乐观锁CAS)

Java中确保线程安全的方法包括使用synchronized关键字、Lock接口、ThreadLocal类以及悲观锁和乐观锁CAS(Compare and Swap)等。

1.Synchronized和Lock

使用synchronized关键字可以实现简单的同步机制,通过对方法或代码块加锁来确保线程安全。例如:

public synchronized void synchronizedMethod() {
    // 同步方法体
}

Lock接口提供了更灵活的锁机制,包括显示加锁和释放锁的操作,可以更精确地控制锁的粒度和获取锁的顺序。例如:

private Lock lock = new ReentrantLock();

public void lockMethod() {
    lock.lock();
    try {
        // 加锁后的代码块
    } finally {
        lock.unlock();
    }
}

2.ThreadLocal和同步

ThreadLocal类可以实现线程封闭,将变量与线程绑定,确保每个线程访问的是独立的变量副本,从而避免了多线程并发访问时的竞态条件和数据不一致问题。
使用ThreadLocal类可以减少同步的需求,提高程序的并发性能。例如:

private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

public void threadLocalMethod() {
    int value = threadLocalValue.get();
    // 访问线程本地变量value
    threadLocalValue.set(value + 1);
}

3.悲观锁和乐观锁CAS

悲观锁是一种悲观地认为数据会被其他线程修改的锁机制,需要在访问共享资源之前先获取锁,并且在整个操作过程中都持有锁。
乐观锁CAS是一种乐观地认为数据不会被其他线程修改的锁机制,先尝试进行操作,然后通过比较交换(Compare and Swap)来确保操作的原子性。
Java中的乐观锁CAS通常是通过Atomic类实现,例如AtomicInteger、AtomicLong等。例如:

private AtomicInteger counter = new AtomicInteger(0);

public void optimisticLockMethod() {
    int oldValue;
    int newValue;
    do {
        oldValue = counter.get();
        newValue = oldValue + 1;
    } while (!counter.compareAndSet(oldValue, newValue));
}

9. 什么是自旋锁?

自旋锁是一种基于忙等待(busy-waiting)的锁机制,在尝试获取锁时,线程不会立即被阻塞,而是以循环的方式不断地尝试获取锁,直到获取成功或者达到一定的尝试次数。
自旋锁适用于锁竞争不激烈的情况下,可以减少线程的阻塞和唤醒开销,提高系统的并发性能。但在锁竞争激烈或持有锁时间较长的情况下,应该谨慎使用自旋锁,避免出现性能问题。

自旋锁的基本思想是,当一个线程尝试获取锁时,如果发现锁已经被其他线程持有,它并不立即进入阻塞状态,而是在循环中不断尝试获取锁,直到获取成功或者超过一定的尝试次数后才放弃。

自旋锁的优点
在锁竞争不激烈的情况下,自旋锁可以减少线程的切换次数,避免了线程进入阻塞状态带来的额外开销。
自旋锁的等待时间通常很短,不会引起线程上下文切换,适用于锁竞争不激烈、且持有锁的时间较短的情况。

自旋锁的缺点
在锁竞争激烈的情况下,自旋锁可能会导致线程长时间的忙等待,占用CPU资源,降低系统的整体性能。
自旋锁不适用于持有锁时间较长或者锁竞争激烈的场景,容易导致线程饥饿和性能下降。

10. 线程的5种状态?

Java中的线程有五种状态:新建、就绪、运行、阻塞和死亡。这些状态描述了线程在执行过程中的不同阶段和状态。

1.新建(New): 当线程对象被创建但尚未启动时,线程处于新建状态。即线程对象被创建后,可以调用start()方法启动线程,使其进入就绪状态。

2.就绪(Runnable): 当线程启动后,它可能进入就绪状态,表示线程已经被创建,但尚未分配CPU执行时间。线程处于就绪状态时,可能正在等待系统资源,例如CPU时间片,一旦获取到CPU时间片,就会进入运行状态。

3.运行(Running): 当线程获得CPU时间片并开始执行时,线程处于运行状态。在运行状态下,线程正在执行它的任务代码。

4.阻塞(Blocked): 当线程被阻塞时,表示线程暂时无法执行,通常是由于某些原因导致线程无法继续执行。例如,线程可能因为等待某个输入/输出操作完成、等待获取锁、等待某个条件满足等原因而被阻塞。一旦等待的条件满足,线程就会进入就绪状态,等待系统重新调度。

5.死亡(Terminated): 当线程的run()方法执行完毕或者调用了线程的stop()方法使线程终止时,线程进入死亡状态。在死亡状态下,线程对象已经被销毁,不再存在。

11. 什么是守护线程?

守护线程(Daemon Thread)是一种在后台提供服务的线程,其优先级比普通线程低,当所有的非守护线程结束时,守护线程也会自动结束。它通常用于执行一些后台任务或者提供一些服务性质的操作,例如垃圾回收器、定时任务等。

守护线程的特点

  • 低优先级: 守护线程的优先级通常较低,这意味着在系统资源有限时,守护线程很可能会被系统优先调度。

  • 随着非守护线程的结束而结束: 当所有的非守护线程都执行完毕或者主动终止后,JVM会自动退出,不会等待守护线程的结束。这意味着守护线程通常用于在程序运行时提供服务,但不影响程序的正常运行结束。

  • 提供后台服务: 守护线程通常用于执行一些后台任务,例如垃圾回收器、定时任务、日志记录等。

守护线程的创建方法与普通线程相同,通过Thread类的构造函数或者实现Runnable接口创建,然后调用setDaemon(true)方法将线程设置为守护线程。

Thread daemonThread = new Thread(() -> {
    // 守护线程的任务代码
});
daemonThread.setDaemon(true);
daemonThread.start();

需要注意的是,守护线程不能持有程序中的资源,例如打开的文件或者数据库连接等资源,因为当所有的非守护线程结束时,守护线程会被强制终止,可能会导致资源泄漏或者未完成的操作。因此,在创建守护线程时,需要注意其执行的任务不依赖于外部资源或者确保在程序结束时能够正确释放资源。

12. Java实现多线程的方式(有程序/两种实现的区别)?

在Java中,实现多线程的方式主要有两种:继承Thread类和实现Runnable接口。下面分别介绍这两种方式以及它们之间的区别。

1.继承Thread类
通过继承Thread类并重写其run()方法来实现多线程,具体步骤如下:

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务代码
        System.out.println("线程执行任务");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

2.实现Runnable接口
通过实现Runnable接口并将其传递给Thread类的构造方法来实现多线程,具体步骤如下:

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务代码
        System.out.println("线程执行任务");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程
    }
}

区别

1.继承Thread类的方式:

  • 线程类直接继承自Thread类,但由于Java是单继承的,因此如果继承Thread类就无法继承其他类。
  • 在实际开发中,如果类已经继承了其他类或者希望将多个线程共享相同的任务代码,不适合使用这种方式。

2.实现Runnable接口的方式:

  • 线程类实现了Runnable接口,可以继续继承其他类,具有更好的灵活性。
  • 多个线程可以共享相同的Runnable对象,实现了线程的共享。

一般来说,推荐使用实现Runnable接口的方式来实现多线程,因为它具有更好的扩展性和灵活性,能够更好地支持对象的多态性。

13. 进程间的通信?

进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和共享信息的过程。在操作系统中,进程间通信是实现多任务和协作的重要机制之一。常见的进程间通信方式包括以下几种:

1.管道(Pipe):管道是一种最基本的进程间通信机制,用于在具有亲缘关系的进程之间进行通信。它可以是半双工的(只能单向传输数据)或全双工的(能够双向传输数据)。
管道主要适用于父子进程或兄弟进程之间的通信。

2.命名管道(Named Pipe):命名管道也是一种进程间通信机制,它与普通管道的区别在于可以通过文件系统路径名来标识,并且不受进程亲缘关系的限制。
命名管道适用于无亲缘关系的进程之间的通信。

3.消息队列(Message Queue):消息队列是一种进程间通信的方式,允许一个进程向另一个进程发送数据块,而且发送和接收的过程是异步的。
消息队列可以实现多对多的通信,消息的发送和接收是基于消息队列标识符的。

4.共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,它允许多个进程共享同一块物理内存区域,使得多个进程可以直接访问共享的数据。
共享内存适用于需要频繁交换大量数据的场景,但需要额外的同步机制来保证数据的一致性。

5.信号量(Semaphore):信号量是一种进程间同步和互斥的机制,用于控制对共享资源的访问。
信号量通常用于控制临界区的访问、进程的同步和互斥等。

6.套接字(Socket):套接字是一种基于网络通信的进程间通信方式,它允许不同主机上的进程进行通信。
套接字通常用于在网络中进行进程间通信,例如客户端和服务器之间的通信。

14. 线程间通信方式(wait和notify)?

在Java中,线程间通信可以通过wait()和notify()方法来实现。这两个方法通常用于在多线程环境下实现线程之间的协调和通知,允许线程在特定条件下等待或被唤醒,从而更好地实现线程间的同步。

wait()方法
wait()方法使当前线程进入等待状态,并释放对象的锁。当线程调用wait()方法时,它会一直等待,直到其他线程调用相同对象上的notify()或notifyAll()方法来唤醒它,或者等待时间到达。
wait()方法通常与条件判断一起使用,例如在同步块中:

synchronized (sharedObject) {
    while (!condition) {
        sharedObject.wait();
    }
    // 执行线程需要执行的任务
}

notify()方法
notify()方法用于唤醒处于等待状态的单个线程。当线程调用notify()方法时,它会随机选择一个处于等待状态的线程唤醒,使其从wait()方法返回。如果没有处于等待状态的线程,notify()方法不会产生任何影响。
notifyAll()方法用于唤醒所有处于等待状态的线程。当线程调用notifyAll()方法时,它会唤醒所有处于等待状态的线程,使它们从wait()方法返回。通常,在更新了共享变量的状态后,调用notify()或notifyAll()方法来通知其他线程。

使用wait()和notify()方法时,需要遵循以下几点

  • 调用wait()和notify()方法的线程必须持有对象的锁。
  • wait()和notify()方法必须在同步块或同步方法中调用。
  • wait()方法和notify()方法必须在同一对象上调用。

15. 说一下线程池?

线程池(ThreadPool)是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源利用率。线程池通过预先创建一定数量的线程,并将它们保存在池中,根据需要重复使用这些线程,从而避免了频繁创建和销毁线程的开销。

线程池通常由以下几个组件构成

1.任务队列(Task Queue):用于存放待执行的任务,线程池中的线程会从任务队列中获取任务并执行。任务队列可以是有界队列(如ArrayBlockingQueue)或无界队列(如LinkedBlockingQueue),有界队列可以避免无限制的任务提交导致内存溢出。

2.线程池管理器(ThreadPool Manager): 负责创建、管理和维护线程池,包括线程的创建、销毁和重用,以及监控线程池的运行状态。

3.线程工厂(Thread Factory):用于创建线程池中的线程,通常是实现了ThreadFactory接口的类。线程工厂可以自定义线程的创建方式,例如设置线程的名称、优先级、守护状态等。

4.拒绝策略(Rejected Execution Handler):当任务队列已满且无法接受新的任务时,拒绝策略定义了线程池应该采取的处理方式。常见的拒绝策略包括抛出异常、丢弃任务、丢弃最旧的任务等。

线程池的优点

  • 降低线程创建和销毁的开销:通过重复利用已创建的线程,减少了线程创建和销毁的开销。
  • 提高系统性能:通过合理地配置线程池大小和任务队列容量,可以更有效地利用系统资源,提高系统的整体性能。
  • 提高代码的可管理性:通过统一管理线程的创建、销毁和执行过程,简化了多线程编程的复杂性,提高了代码的可维护性和可读性。

Java中的线程池通常通过Executor框架来实现,包括ExecutorService接口及其实现类ThreadPoolExecutor。通过ExecutorService接口,可以方便地提交任务、关闭线程池、获取线程池的运行状态等。Java提供了一些内置的线程池实现,如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等,也可以根据实际需求自定义线程池。

16. sleep和wait异同?

sleep()和wait()都可以用于线程的暂停,但是它们的使用场景和调用方式不同,sleep()主要用于简单的暂停执行,而wait()主要用于线程间的协作。

相同点

  • 都可以暂停线程的执行: sleep()和wait()都可以使当前线程暂停一段时间的执行。

  • 都会释放对象的锁: 调用sleep()或wait()方法时,都会释放对象的锁,让其他线程有机会获取该对象的锁。

不同点

  • 方法来源不同:
    sleep()方法来自于Thread类,而wait()方法来自于Object类。

  • 使用场景不同:
    sleep()方法通常用于让当前线程暂停执行一段指定的时间,是一种简单的暂停执行方式。例如,可以用于模拟程序中的等待或延迟操作。
    wait()方法通常用于线程间的协作,让线程在等待某个条件满足时暂停执行,并释放对象的锁。通常结合条件判断和唤醒操作一起使用。例如,线程A等待某个条件的满足,线程B在条件满足时通过notify()或notifyAll()方法唤醒线程A。

  • 调用方式不同:
    sleep()方法是static方法,直接通过Thread.sleep()调用。
    wait()方法必须在synchronized同步块或方法中调用,否则会抛出IllegalMonitorStateException异常。

  • 唤醒方式不同:
    调用sleep()方法后,线程会在指定时间后自动恢复执行,不需要其他线程的干预。
    调用wait()方法后,线程会进入等待状态,需要其他线程调用相同对象上的notify()或notifyAll()方法来唤醒。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值