Java八股文面试题之Java并发/多线程(一)

您所在的位置:网站首页 java高级编程题库线程图 Java八股文面试题之Java并发/多线程(一)

Java八股文面试题之Java并发/多线程(一)

2023-06-20 01:36| 来源: 网络整理| 查看: 265

目录

1.Java的并发和并行指的是什么?

2.ThreadLocal的应用场景有哪些?

3.使用ThreadLocal可能会出现哪些问题?

4.ThreadLocal的底层是如何实现的?

5.谈谈你对java多线程的了解?哪种方式比较常用?为什么?

6.Java中创建一个线程池的方式有哪些?

7.为什么不建议使用Executors来创建线程池?

8.sychronized和ReentrantLock有哪些不同点?

9.ReentrantLock分为公平锁和非公平锁,底层分别是如何实现的?

10.Java中常见的锁有哪些?

11.常见的线程池有哪几种?

12.线程有哪些基本状态?

13.线程池有哪些状态? 

14.连接池有哪些参数? 

15.线程池中提交一个任务的流程是怎样的? 

16.为什么不建议用stop()方法去停止一个线程? 应该如何停止一个线程?

1.Java的并发和并行指的是什么?

并发:指的是在单个处理器上同时执行多个任务,通过快速切换上下文,使得每个任务看上去都是同时运行。Java中并发通过线程实现,在Java中多个线程交替执行,共享CPU资源,每个线程都是独立运行的。并发是基于资源共享的前提,用于提高程序的资源的利用率,提高程序的并发性和吞吐量。

并行:指的是真正的多任务运行,通过利用多个处理器同时执行不同的任务,将一个大任务分解为多个小任务,并行的运行在多个处理器上,从而显著的提高了程序的处理能力和速度。Java中的并行可以通过多线程、分布式处理等技术实现,最后把执行的结果合并起来,以达到并行加速的效果。

总结:并发适用于资源共享的场景,可以通过线程实现;并行适用于大量数据处理的场景,可以用多线程、分布式处理来实现。

2.ThreadLocal的应用场景有哪些?

线程安全的实现 ThreadLocal可以让每个线程拥有自己的变量实例,从而避免了线程安全问题。比如在Web应用中,可以使用ThreadLocal保存一些请求相关的信息,如用户ID、Session等。

数据库连接管理 在数据库连接池中,为了保证连接不被线程之间共享,通常会使用ThreadLocal来存储每个线程独有的Connection对象,从而避免多个线程使用同一个Connection对象的问题。

日志跟踪 在日志打印时,可以使用ThreadLocal来保存当前线程的日志输出级别,从而方便地进行日志级别控制。

时间格式化 在多线程环境下,时间格式化也经常会出现线程安全问题,使用ThreadLocal可以让每个线程维护自己的SimpleDateFormat对象,从而避免线程安全问题。

Spring事务处理 Spring的事务处理机制使用ThreadLocal来存储当前事务的状态,从而实现对多个事务的并发处理。

Connection对象:是表示关系型数据库建立连接的对象。通过Connection对象,应用程序可以向数据库服务器发送SQL语句,执行增删改查等。 

SimpleDateFormat对象:是Java中提供的一个日期格式化类,它可以将日期对象格式化为字符串,或将字符串解析为日期对象。

3.使用ThreadLocal可能会出现哪些问题?

内存泄漏 由于ThreadLocal是在当前线程中存储数据,在不及时清理的情况下,可能会造成内存泄漏的问题。如果一个线程长时间存活,并且ThreadLocal实例没有及时删除,那么这个ThreadLocal实例会一直占用内存,直到线程销毁。

线程安全问题 虽然ThreadLocal本身不会出现线程安全问题,但是由于ThreadLocal实例会被多个线程共享,如果在使用ThreadLocal时没有保证线程安全,可能会造成数据的混乱和错误结果输出。

不可靠的继承性 ThreadLocal使用的是线程本地存储的方式,子线程无法直接访问父线程中的ThreadLocal变量,因此线程之间的继承关系是无效的。如果需要子线程也能访问父线程中的ThreadLocal变量,可以通过InheritableThreadLocal实现。

对线程池的影响 使用ThreadLocal对象可能会影响线程池的性能和资源利用率。由于线程池中的多个线程会共享同一个ThreadLocal实例,因此需要慎重考虑ThreadLocal对象的生命周期,避免线程池中的线程间出现相互干扰等问题。

InheritableThreadLocal:InheritableThreadLocal继承自ThreadLocal类,但重写了它的一些方法,在设置和获取ThreadLocal变量时会自动处理线程之间的继承关系。在父线程和子线程之间,通过InheritableThreadLocal变量,可以轻松地传递上下文信息和共享状态。

4.ThreadLocal的底层是如何实现的?

ThreadLocal的底层主要依赖于Thread类和ThreadLocalMap类实现。当我们使用ThreadLocal实例的set方法设置值时,本质上是在当前线程所绑定的ThreadLocalMap中存储一个键值对,其中键为当前ThreadLocal对象,值为设定的值。每个线程都有自己的ThreadLocalMap实例,因此在线程之间是互不干扰的,数据也无需进行同步,可以提高并发性能。同时,由于ThreadLocalMap对象的生命周期与当前线程一致,一旦线程被销毁,ThreadLocalMap对象也会被回收,不会造成内存泄漏的问题。总之,ThreadLocal主要依赖于Thread类和ThreadLocalMap类来实现,利用线程本地存储的方式,实现了多个线程间共享变量的目的。

5.谈谈你对java多线程的了解?哪种方式比较常用?为什么?

Java多线程是指在同一时间内,一个Java程序中有多个线程同时运行。使用多线程可以提高程序的并发性和性能,因为多个线程可以同时执行不同的任务,从而使得整体的执行效率更高。

在Java中,一般来说实现多线程一共有四种方法:

(1)继承Thread类,重写run()方法

(2)实现Runnable接口

(3)实现Callable接口。需要使用Future接口来接收子线程的返回值

(4)基于线程池实现

对于该知识点的代码详情,可以查看如下文章:Java基础知识学习——线程_什么时候才能变强的博客-CSDN博客

那种方式比较常用?为什么?

我个人认为是基础线程池的实现最常用。原因如下:

线程池可以重复利用线程,避免了频繁地创建和销毁线程所带来的开销,对于高并发的应用尤为重要。

线程池能够控制线程的数量,避免线程数量过多而导致系统崩溃或性能下降。通过设置合适的线程数量,可以更好地利用CPU和内存资源。

使用线程池可以更方便地管理线程,比如可以对线程进行优先级设置、设置线程池等待时间等,更加灵活和高效。

相比之下,继承Thread类和实现Runnable接口实现多线程的方式缺乏灵活性,而实现Callable接口需要和Future接口结合使用,相对比较复杂。

6.Java中创建一个线程池的方式有哪些?

使用Executors工具类提供的静态方法newCachedThreadPool来创建线程池,如:

ExecutorService executorService = Executors.newCachedThreadPool(10);

直接创建ThreadPoolExecutor线程池,如:

ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue());

使用Spring框架提供的线程池配置类ThreadPoolTaskExecutor创建线程池,如:

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix(prefix); executor.initialize(); 7.为什么不建议使用Executors来创建线程池? 当我们使用Executors来创建FixedThreadPool时,通过查看源码可以知道,对应的构造方法一共有两个,一个是只传递线程池大小,另外一个需要传递线程池大小和一个ThreadFactory。如果使用Executors的固定配置来创建线程池,就意味你无法为每个任务提供最佳的线程池大小,会造成一个性能问题。并且它创建的队列为LinkedBlockingQueue,这是一个无界的阻塞队列,如果任务过多就会导致队列占用的内存过多,最终导致OOM。当我们使用Executors来创建SingleThreadExecutor时, 也是用的这个无界的阻塞队列,也会导致上述同样的问题。

总结:原因有二。

第一,是使用无界的阻塞队列,可能导致OOM。

第二,是使用一个参数的构造方法时,会导致不能自定义线程的名字,不利于排查问题。

8.sychronized和ReentrantLock有哪些不同点?

Synchronized和ReentrantLock都是Java中用来实现线程同步的机制,它们都能够保证同一时刻只有一个线程执行关键代码段。它们的不同点如下:

sychronized是Java中的一个关键字,而ReentrantLock是JDK提供的一个类。synchronized是隐式锁,在进入synchronized修饰的代码块时自动获取锁,执行完后自动释放锁。ReentrantLock则需要手动获取锁和释放锁,即需要调用lock()和unlock()方法。synchronized是JVM层面实现的内置锁(也称为监视器锁),而ReentrantLock是在API层面实现的可重入锁。synchronized是非公平锁,ReentrantLock可以指定公平锁或非公平锁。synchronized锁的是对象,锁信息保存在对象头中,ReentranLock是根据int类型的state标识锁的状态。synchronized的底层有一个锁升级的过程,而ReentrantLock没有。synchronized不支持等待可中断,而ReentrantLock支持。

隐式锁:当一个线程进入使用synchronized所修饰的方法或代码块时,它会自动地获得一个锁,这个被称为隐式锁。线程在退出同步块时,会自动释放锁。此时,其他线程才可以进入该同步块。

监视器锁:是synchronized所实现的锁机制,它是一种内置锁。每个Java对象都可以关联一个监视器锁,当线程进入对象的同步块时,它会尝试获取该对象的监视器锁,如果对象的监视器锁在被其他线程占用时,线程就会进入阻塞状态直到获取到锁。

可重入锁:是指在同一个线程内,可以对同一个锁进行重复获取,不会出现死锁的状态。在Java中,synchronized关键字是可重入的锁,在线程获取此锁后,如果再次获取锁,锁并不会释放。而ReentrantLock是Java API中的一个可重入锁实现。

公平锁/非公平锁:公平锁和非公平锁是指在多线程竞争锁的情况下,是否要按照线程的申请顺序来分配锁。公平锁是指多个线程依次获得锁,效率较低,但是能够保证不会出现线程饥饿的情况。非公平锁则随机分配锁,可能会导致某些线程一直获取不到锁,但因此而带来的吞吐量提高。

锁信息:是指一个锁相关的一些元属性,例如锁状态、锁持有者等等。

锁升级:是指锁从偏向锁、轻量级锁到重量级锁的升级过程。在Java虚拟机中,对于互斥同步,锁的实现有三种:偏向锁、轻量级锁和重量级锁(下文讲述),跟随竞争的激烈程度而逐渐升级。这个过程被称为锁升级。

等待可中断:是指当线程进入等待状态时,可以通过中断该线程来使线程从等待过程中退出,也就是说从一个取消了的锁获取中断。 比如当一个线程申请锁,但由于该锁被其他线程占用而无法获取,线程就会进入等待状态。在等待过程中,如果其他线程中断了该线程,则该线程会从等待中退出。而synchronized不支持等待可中断,而ReentrantLock支持。

9.ReentrantLock分为公平锁和非公平锁,底层分别是如何实现的?

公平锁:是指按照线程的申请顺序来获取锁,即先申请锁的线程先获取锁,在ReentranLock中,公平锁是通过内部类FairSync实现的,FairSync类继承了Sync类(是一个内部类),Sync类又继承了AQS(AbstractQueuedSynchronized)类,通过AQS提供过的state变量和FIFO队列来实现线程的排队和锁的获取和释放。

非公平锁:是指锁的获取是无序的,不考虑等待时间,能够优先获取到锁的线程将直接获得锁而无需进入等待队列。这种方式有可能会导致后面申请的线程始终获取不到锁,甚至出现"饥饿"现象。在ReentrantLock中,非公平锁是通过内部类NonfairSync实现的,默认情况下采用非公平锁来获取锁。该类同样继承了AQS类,并重写了父类中的tryAcquire方法,实现了一种非公平的获取锁方式

10.Java中常见的锁有哪些?

synchronized关键字:synchronized关键字是Java中内置的锁机制,可以用于方法上和代码块上。synchronized锁的是对象,通过对象的监视器实现线程同步,支持可重入锁和非公平锁。

ReentrantLock类:ReentrantLock是Java API提供的一个可重入、可中断、带有公平和非公平选项的锁,适合高度竞争和高度并发的场景。

ReentrantReadWriteLock类:ReentrantReadWriteLock实际上是两个锁,一个读锁和一个写锁,它们互斥,支持同步读和排它写,适合读密集、写少量的场景。

StampedLock类:StampedLock是Java API提供的一种基于乐观锁的机制,支持三种模式:读锁、写锁和乐观读锁,适合读多写少的场景。

LockSupport类:LockSupport类提供一种基本的线程阻塞和唤醒的机制,它可以实现类似于wait()和notify()的操作,但不需要在synchronized块中执行。LockSupport.park()可以使线程阻塞,LockSupport.unpark()可以唤醒指定的线程。

Semaphore类:Semaphore是一种基于计数的信号量,它可以控制同时访问某个资源的线程数量。

CountDownLatch类:CountDownLatch是一种基于计数的同步工具,可以用于协调多个线程之间的同步,它允许一个或多个线程等待其他线程完成操作后再继续执行。

CyclicBarrier类:CyclicBarrier是一种同步工具,在达到指定的参与者数量后,会执行一个屏障任务,可以用于等待多个线程到达某个状态,然后再同时继续执行。

这些锁机制都可以保证同一时刻只有一个线程执行关键代码段,从而避免了多线程并发时的数据竞争问题,提高系统的并发性能。不同的锁机制适用于不同的场景,开发者可以根据具体情况选择合适的锁实现。

11.常见的线程池有哪几种?

FixedThreadPool:固定大小线程池,可以指定线程池的大小,当线程池的线程数量达到指定大小时,新的任务就会处于等待状态,直到有线程空闲出来。

CachedThreadPool:可缓存线程池,线程数没有限制,当有空闲线程时就会复用已有的线程,当没有可用的线程时就会创建新的线程。

SingleThreadPool:单线程线程池,只有一个线程执行任务,若该线程异常结束,会重新创建一个新的线程来执行后面的任务。

ScheduledThreadPool:定时器线程池,支持定时以及周期性执行任务。

12.线程有哪些基本状态?

新建(New):当线程被创建时,进入新建状态。

运行(Runnable):线程已经启动,可以被CPU调度执行。可以接受新任务并且会处理队列中的任务。

阻塞(Blocked):该线程因为某些原因(如等待输入输出、网络请求等)被暂停了,不会接受新任务,但是会处理队列中的任务,任务处理完后会中断所有的线程。

等待(Waiting):当线程等待某个条件时,例如调用了Object.wait()方法进入等待状态,此时线程会释放掉获取的锁,并且需要被其他线程唤醒才能继续执行。

计时等待(Timed Waiting):当线程调用带有超时时间的阻塞方法时,例如Thread.sleep()、Object.wait(long)方法时,线程会进入计时等待状态,同时也会释放获取的锁,并在一段时间后自动返回Runnable状态。

终止(Terminated):当线程执行完毕或者是因为异常而结束时,线程进入终止状态。这时候线程不会回到Runnable状态,也不可再次启动。

13.线程池有哪些状态? 

RUNNING:表示线程池处于运行状态,可以接收新任务,并处理等待队列中的任务。

SHUTDOWN:表示线程池不再接收新任务,但会继续处理等待队列中的任务。

STOP:表示线程池不再接收新任务,且会中断正在处理的任务。

TIDYING:表示线程池中所有任务已经执行完毕,并进入到该状态的线程池会执行 termination 方法。

TERMINATED:表示线程池已经被终止,线程池中不再有任何活跃的任务。

除了 RUNNING 状态以外,其他状态下线程池都不会接收新的任务 

14.连接池有哪些参数?  连接池大小(maximumPoolSize):最多能同时存在的连接数。最小空闲连接数(minimumIdle):连接池维护的最小空闲连接数。连接超时时间(connectionTimeout):连接池尝试获取连接的最长时间。空闲连接超时时间(idleTimeout):连接池维护的连接在没有被使用时的最长存活时间。最大生命周期(maxLifetime):连接池维护的连接的最长生命周期,超过这个时间的连接会被强制关闭。连接初始化执行的 SQL 语句(connectionInitSql):连接池初始化连接时需要执行的 SQL。连接测试语句(connectionTestQuery):连接池用于测试连接是否可用的 SQL 查询语句。是否自动提交(autoCommit):连接池中的连接是否自动提交事务。池化的 Statement 的类型(poolPreparedStatements):是否缓存 PreparedStatement。批处理的 SQL 数量(maxOpenPreparedStatements):当缓存 PreparedStatement 时,最多缓存多少条。 15.线程池中提交一个任务的流程是怎样的?  调用exectue方法,并且提交一个Runnable对象,然后查看是否有空闲的线程判断当前线程池中的线程数是否小于核心线程数量,如果小于,则创建新线程执行Runnable,如果大于等于,则尝试将Runnable加入到workQueue中,进行下一步判断如果workQueue没满,则Runnable正常入队,等待执行,如果workQueue满了,则入队失败,尝试继续增加线程,进行下一步判断如果线程池中的线程数小于最大线程数,则创建线程并且执行任务,如果大于等于最大线程数,则拒绝该Runnable 16.为什么不建议用stop()方法去停止一个线程? 应该如何停止一个线程?

使用stop()方法去停止一个线程,可能会导致以下问题:

stop() 方法会强制结束线程,无论它当前执行到什么位置,都会立即停止线程。这可能会导致线程正在执行的任务被中断,并且资源没有得到及时释放,造成资源泄漏。

使用 stop() 方法可能会导致对象处于一种不一致的状态,因为线程可能在不安全的环境下结束,没有来得及进行资源释放和数据同步等操作。

stop() 方法可能会导致一些清理性工作没有完成,比如线程没有执行 finally 块中的代码,因此也可能会带来意外的后果。

并且stop()方法会释放一些锁,例如synchronized。

interrupt() 方法则更为安全和可控,它会向目标线程发送一个中断信号。目标线程可以根据自己的实现来决定如何响应中断信号,并进行必要的资源释放和清理操作。同时,由于 interrupt() 方法并不会强制结束线程,因此也不会产生死锁或者资源争用问题。

但是需要的是,如果你在线程中执行了阻塞方法,可能就会导致抛出异常,并且把接受到的信号重新设置为false,如果你获取到了异常但是并没有抛出异常,就会导致线程实际上还是没有接受到中断信号。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3