java并发八股

前述:Ⅰ.⭐️代表面试高频,不要错过。Ⅱ.❌代表可不看。Ⅲ.没有符号标注即为常规基础

1.三种线程初始化⽅法( Thread 、 Callable , Runnable )区别

主要有三种方式创建线程,分别是继承Thread类并重写run方法;实现Runnable接口并实现其run方法,然后通过Thread类的构造函数将runnable对象传递给线程;实现Callable接口并实现call方法,通过ExecutorService的submit方法提交callable对象。

继承Thread类的方式的话缺点就是不支持多重继承。

实现Runnable接口的话,优点就是可以继承其他类。

实现callabe接口的话,可以获取callable的返回值或处理异常

2.线程池( ThreadPoolExecutor ,7⼤参数,原理,四种拒绝策略,四个变型:Fixed,Single,Cached,Scheduled)⭐️⭐️

创建线程池有两种方式,其一就是通过ThreadPoolExecutor构造函数来创建,其二就是通过Executor框架的工具类Executors来创建。我们可以创建四种类型的ThreadPoolExecutor,分别为FixedTheadPool,SignleThreadEXecutor,cachedThreadPool,ScheduledThreadPool。**FixedThreadPool有一个固定线程数量的线程池。当有一个新任务提交时,线程池中若有空闲线程,就立即执行;若没有,则暂存到任务队列。SingleThreadExecutor只有一个线程的线程池。若多于一个任务被提交到该线程池,任务会被保存在一个任务队列中。CachedThreadPool就是可以根据实际情况调整线程数量的线程池。当有新任务提交时,如果当前线程池中没有线程可用,他就会创建一个新的线程来处理该任务,线程在空闲一段时间后会被销毁。ScheduledThreadPool就是在给定的延迟后运行任务或者定期执行任务的线程池。

2.1使用的队列

FixedThreadPool 和 SingleThreadExecutor使用的是无界队列,CachedThreadPool使用的是同步队列,ScheduledThreadPool使用的是无界的延迟阻塞队列。

ThreadPoolExecutor threadPool =  new ThreadPoolExecutor(properties.getCorePoolSize(),
                properties.getMaxPoolSize(),
                properties.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(properties.getBlockQueueSize()),
                Executors.defaultThreadFactory(),
                handler);

2.2参数⭐️

最核心的参数就是核心线程数,最大线程数,任务队列。除此之外还有保活时间、保活时间的单位、线程工厂、拒绝策略。

2.3线程池的参数如何配置

不同的业务场景,线程池参数的配置情况也不同。比如说追求快速响应用户请求的场景,最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。而对于快速处理批量任务的场景,要关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数,并且设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

2.4如何设定线程池的大小?

CPU 密集型任务的话线程数就少一点,一般为N+1;I/O 密集型任务的话线程数就多一点,一般为2N

2.5什么场景要用到线程池/多线程

(1)异步任务的场景,比如说短信、邮件等通知类业务。

(2)并行计算的场景,将一个大任务拆成多个小任务,最后再将多个子任务计算的结果汇总。

2.6四种拒绝策略

分别是AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy;分别是抛出RejectedExecutionException异常来拒绝执行任务的处理;调用执行自己的线程运行任务;直接丢弃;选择最早的未处理的任务丢弃

2.7线程池的关闭流程通常包括以下几个步骤:

  1. 拒绝新任务:线程池会停止接受新任务。
  2. 处理已提交任务:线程池会继续处理已经提交但尚未完成的任务。
  3. 终止空闲线程:线程池会终止那些空闲的线程。
  4. 等待正在执行的任务完成:线程池会等待正在执行的任务完成,然后终止这些线程。

2.8直接重启服务器,线程池的关闭流程是怎么样的?

JVM 不会执行上述优雅的关闭流程,所有线程会被强制终止。这可能导致以下问题:

  • 任务丢失:正在执行的任务可能无法完成。
  • 资源泄漏:未关闭的资源(如文件句柄、数据库连接)可能会泄漏。
  • 数据不一致:如果任务涉及数据写入操作,可能会导致数据不一致。

因此,建议在重启服务器前,手动关闭线程池并确保所有任务完成

2.9优点‼️

1.复用线程,降低资源消耗

2.提高响应速度

3.管控线程数和任务数

2.10如何停止线程池‼️

调用线程池的 shutdown() 方法来关闭线程池。该方法会停止线程池的接受新任务,并尝试将所有未完成的任务完成执行;

调用线程池的 shutdownNow() 方法来关闭线程池。该方法会停止线程池的接受新任务,并尝试停止所有正在执行的任务。该方法会返回一个未完成任务的列表,这些任务将被取消。

2.11如何判断线程池中的任务已执行完‼️

  1. 使用 getCompletedTaskCount() 统计已经执行完的任务,和 getTaskCount() 线程池的总任务进行对比,如果相等则说明线程池的任务执行完了,否则既未执行完。
  2. 使用 FutureTask 等待所有任务执行完,线程池的任务就执行完了。
  3. 使用 CountDownLatch 或 CyclicBarrier 等待所有线程都执行完之后,再执行后续流程。

2.12核心线程数为0时,线程池如何执行‼️

当核心线程数为 0 时,当来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务

2.13 提交给线程池中的任务可以被撤回吗?

可以,当向线程池提交任务时,会得到一个Future对象。这个Future对象提供了几种方法来管理任务的执行,包括取消任务。

3.阻塞队列,⼿写 BlockingQueue 。

3.1阻塞队列有什么好处⭐️

  • 在不使用阻塞队列的情况下,如果队列为空或者为满,消费者线程或者生产者线程就需要不断地轮询忙等,这种忙等(busy-waiting)不仅浪费CPU资源,还降低了系统的整体性能。
  • 线程安全:阻塞队列内部通过锁(如互斥锁)和条件变量等机制保证了队列的操作(如入队和出队)的线程安全

3.2⼿写 BlockingQueue⭐️

public class BlockingQueue<T>{
    private Queue<T> queue;
    private int capacity;
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    
    public BlockingQueue(int capacity){
        this.capacity = capacity;
        this.queue = new LinkedList<>();
    }
    public void put(T item) throws InterruptedException {
        lock.lock();
        try{
			while(queue.size()==capacity){
				notFull.await();
            }
            queue.add(item);
            notEmpty.signal();
        }finally{
            lock.unlock();
        }
    }
    public T take() throws InterruptedException{
		lock.lock();
        try{
			while(queue.isEmpty()){
                notEmpty.await();
            }
            T item = queue.remove();
            notFull.signal();
            return item;
        }finally{
            lock.unlock();
        }
    }
}

4.乐观锁:CAS(优缺点,ABA问题,DCAS)⭐️⭐️

4.1乐观锁

乐观锁总是假设最好的情况,每次去拿数据的时候都认为对方不会修改,所以不会加锁。只有在更新的时候,才会判断是否已被对方修改。乐观锁主要的实现就是版本号机制和cas。

4.2乐观锁的应用场景⭐️

  1. 读多写少的场景:在大多数操作是读取而非写入的情况下,乐观锁可以减少锁的开销,提高系统性能。
  2. 并发冲突较少的场景:当并发冲突较少时,乐观锁可以减少锁的等待时间,提高系统吞吐量。
  3. 需要高性能的场景:乐观锁避免了传统锁机制的开销,适用于对性能要求较高的系统。
  4. 数据库更新操作:在数据库中,乐观锁常用于更新操作,通过版本号或时间戳来控制并发更新。

4.3cas

cas就是比较和替换。是一个原子操作,主要包含预期值、变量值和新值。核心思想就是,当变量的值和预期值相等时,才用新值替换掉变量的值,否则就是已被其他线程修改,就失败重试。

4.4cas的缺点:

  • aba问题,针对aba问题的解决方案就是加一个时间戳或者版本号,后来jdk1.5引入了AtomicStampedRdfference就解决了这个问题。它就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 循环时间长开销大。
  • 只能保证一个共享变量的原子操作。

4.5应用场景

乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量。

5.悲观锁:Synchronized ⭐️

5.1作用

synchronized可以限制对某个资源的访问,但是它锁的并不是资源本身,可以锁住某个对象,只有线程拿到这把锁之后才能够去访问临界资源。

5.2特性

  • 可重入
  • 异常会释放锁

​ 5.3使⽤:⽅法(静态,⼀般⽅法),代码块(this, ClassName.class )

​ 可用修饰实例方法、静态方法、代码块。

​ 5.4优化:锁粗化,锁消除,⾃适应⾃旋锁,偏向锁,轻量级锁

​ 在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术

​ 5.5锁升级的过程和细节:⽆锁->偏向锁->轻量级锁->重量级锁(不可逆)⭐️

  • synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,默认也就是使用偏向锁。
  • 当第二个线程来争用的时候,此时第二个线程会占用cpu,循环等待锁的释放,这时候偏向锁也就升级为自旋锁。
  • 当自旋10次之后,就会升级为重量级锁,重量级锁是不占用cpu,他会讲自己挂起,等待被唤醒。当线程数较少、运行时间较短的时候是比较适合使用自旋锁,反之则比较适合重量级锁。

​ 5.6重量级锁的原理( monitor 对象, monitorenter , monitorexit )

​ synchronized同步语句块的实现使用的就是mointorenter和monintorexit指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取对象监视器 monitor 的持有权。

synchronized 修饰的方法时,通过 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法

​ ⭕️5.7 ReentrantLock :和 Synchronized 区别?(公平锁、⾮公平锁、可中断锁....)、原理、⽤法⭐️

​ 介绍:ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁

​ 原理:ReentrantLock 里面有一个内部类 SyncSync 继承 AQS,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。

​ 两者都是可重入锁,但是ReentrantLock 增加了很多高级功能,比如支持公平锁和非公平锁。等待可中断。通过创建多个condition可以实现有选择性的通知。

应用场景

悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的

6.ThreadLocal :底层数据结构: ThreadLocalMap 、原理、应⽤场景。⭐️⭐️

6.1ThreadLocal 是什么?

就是线程本地变量,如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

6.2底层数据结构

ThreadLocal的底层数据建构就是ThreadLocalMap,它的key就是ThreadLocal的引用,值就是Object对象。

6.3应用场景

(1)在线程池执行多个任务时,可以使用ThreadLocal存储每个线程独有的数据,避免线程间的数据冲突。(2)在web应用程序中,ThreadLocal可以用来存储当前请求的上下文信息,使得不同请求之间相互独立(3)在使用数据库连接池时,使用ThreadLocal将连接池中的数据库连接与当前线程绑定,确保每个线程都能得到自己独有的数据库连接。

6.4存在的问题

存在内存泄漏,因为key是ThreadLocal的弱引用,而value是强引用,如果被垃圾回收,key就会被清理掉,这样就会出现key为null的entry,value就永远不会被gc回收。解决办法就是在set、get、remove方法的时候,清理掉key为null的记录。

7.Atomic 类(原理,应⽤场景)

atomic是原子的意思,就是说在多线程的环境下,一个操作不能被其他线程中断。Atomic类位于juc的atomic包下,主要有基本类型、数组类型、引用类型。

原理:基于cas来保证原子性。

应用场景:应用在需要操作为原子性的场景。比如说i++,就可以使用AtomicInteger来保证原子性。

8.AQS:原理、 Semaphore 、 CountDownLatch 、 CyclicBarrier⭐️⭐️⭐️

aqs就是抽象队列同步器,主要用来构建锁和同步器,比如说reentrantLock,semaphore,countDownLatch,CyclicBarrier等。aqs的核心思想就是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态。

原理:AQS 使用 int 成员变量 state 表示同步状态,并且使用CLH队列将暂时获取不到锁的线程加入到队列中。CLH队列是一个虚拟的双向队列,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

9.Volatile :原理:有序性,可⻅性⭐️

volatile修饰变量,就表示这个变量是共享的且不稳定的,每次使用都要去主存中进行读取。可以保证变量的可见性,并且禁止指令重排序。

9.1原理:

1.volatile 内存可见性主要通过 lock 前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通过 MESI 协议使其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。

2.volatile 的有序性是通过插入内存屏障(Memory Barrier),在内存屏障前后禁止重排序优化,以此实现有序性的。

9.2synchronized 和 volatile区别⭐️

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

10.什么是线程安全,如何保障线程安全?⭐️

线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。

保障的话可以悲观锁的机制,比如说reentrantlock,synchronized。或者乐观锁的机制cas等

11.什么是原子性,可见性,有序性

原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行。

可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个变化。

有序性是指程序执行的顺序与代码中的顺序保持一致。而不受编译器优化、CPU指令重排序等影响。

12.那jvm为什么会打乱我们的指令顺序?

主要是为了优化代码来提高执行的效率

13.为什么会有原子性问题?什么导致的?

因为发生了线程切换,CPU去执行另一项操作,中断了当前线程执行的任务

14.讲讲 Java 提供了哪些锁?它们有什么区别?

主要就是synchronized,ReentrantLock,ReentrantReadWriteLock,StampedLock

(1)synchronized

synchronized是一个可重入且独占式的锁。在早期 属于 重量级锁,而在jdk6之后,synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术。可用于修饰实例方法、静态方法、代码块;synchronized同步语句块的实现使用的就是mointorenter和monintorexit指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取对象监视器 monitor 的持有权。 synchronized 修饰方法时,通过 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法

(2)ReentrantLock⭐️⭐️⭐️

ReentrantLock实现了 Lock 接口,是一个可重入且独占式的锁。ReentrantLock 里面有一个内部类 SyncSync 继承 AQS,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。相比于synchronized,ReentrantLock 增加了很多高级功能,比如支持公平锁和非公平锁。等待可中断。通过创建多个condition可以实现有选择性的通知。

14.1非公平和公平锁具体代码流程,用到的数据结构是什么,具体讲讲

其实就是问的aqs底层怎么实现

(1)非公平锁

非公平锁在获取锁时,不考虑等待时间,任何线程都有机会直接获取锁。

代码流程:1.尝试获取锁:直接尝试获取锁,如果成功则返回;如果失败,则将线程加入等待队列。2.加入等待队列:将当前线程封装成一个节点,加入到等待队列的尾部。3.自旋获取锁:线程在等待队列中自旋,尝试获取锁。4.获取锁成功:从等待队列中移除节点,线程获取锁。

(2)公平锁

公平锁在获取锁时,会考虑等待时间,先等待的线程优先获取锁。

代码流程:1.尝试获取锁:检查等待队列是否为空,如果为空则直接获取锁;如果不为空,则将当前线程加入等待队列。2.加入等待队列:将当前线程封装成一个节点,加入到等待队列的尾部。3.等待获取锁:线程在等待队列中等待,直到轮到自己获取锁。4.获取锁成功:从等待队列中移除节点,线程获取锁。

(3)用到的数据结构

  1. 等待队列: ReentrantLock 使用一个双向链表(FIFO队列)来管理等待的线程。每个节点包含一个线程引用和指向前后节点的指针。
  2. 节点(Node): 每个等待的线程被封装成一个节点,节点包含线程的状态、线程引用、前后节点的指针等信息。
  3. 状态变量: state 变量表示锁的状态,0表示未锁定,大于0表示锁定状态。exclusiveOwnerThread 变量表示当前持有锁的线程。

14.2如果让你设计一个非公平锁,你怎样设计,用到啥数据结构

(3)ReentrantReadWriteLock

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。 它其实是两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。和 ReentrantLock 一样,ReentrantReadWriteLock 底层也是基于 AQS 实现的。也支持公平锁和非公平锁。

(4)StampedLock

StampedLock 是 JDK 1.8 引入的性能更好的读写锁,不可重入不支持条件变量 ConditionStampedLock 并不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 独立实现的。StampedLock 通过 CLH 队列进行线程的管理,通过同步状态值 state 来表示锁的状态和类型。它提供了三种模式的读写控制模式:读锁、写锁和乐观读。相比于传统读写锁多出来的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,所以性能更好。StampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。

为什么stampedLock会导致cpu100%‼️

在 StampedLock 执行 writeLock 或 readLock 阻塞时,如果调用了中断操作,如 interrupt() 可能会导致 CPU 使用率飙升。这是因为线程接收到了中断请求,但 StampedLock 并没有正确处理中断信号,那么线程可能会陷入无限循环中,试图从中断状态中恢复,这可能会导致 CPU 使用率飙升。

17.dcl单例为什么要加Volatile?

Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

18.有A、B、C三个线程,如何保证三个线程同时执行?如何保证三个线程依次执行?如何保证三个线程有序交错执行?

CountDownLatch,CyclicBarrier;Semaphore

19.如何给线程池命名?

(1)利用 guava 的 ThreadFactoryBuilder

(2)自己实现 ThreadFactory,然后重写newThread方法。

20.Future 类有什么用?

Future 类是异步思想的典型运用。当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。

主要包括下面这 4 个功能:取消任务;判断任务是否被取消;判断任务是否已经执行完成;获取任务执行结果。

缺陷:不支持异步任务的编排组合;获取计算结果的 get() 方法为阻塞调用。

21.CompletableFuture 类有什么用?

CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。

22.现在创建十个线程,每个线程有一个跑步的方法,如何保证十个线程同时执行跑步方法,保证赛跑的公平性?

做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

附:本文内容来自javaguide、小林coding、牛客面经、模型生成、个人总结等。

#实习##春招##秋招##八股#

学习之道我知道 文章被收录于专栏

博主博主,网上八股那么多,不知道看哪个怎么办,有没有什么重点的八股拿来学习一下的? 有的,兄弟有的! 会陆续发布一些本牛在实习和秋招过程中总结的八股。

全部评论
点赞 回复 分享
发布于 02-24 16:55 四川

相关推荐

评论
12
77
分享

创作者周榜

更多
牛客网
牛客企业服务