这几道多线程面试题都不会,怎么拿Java大厂的offer?

1、为什么用线程池?

  有时候,系统需要处理非常多的执行时间很短的请求,如果每一个请求都开启一个新线程的话,系统就要不断的进行线程的创建和销毁,有时花在创建和销毁线程上的时间会比线程真正执行的时间还长。

  而且当线程数量太多时,系统不一定能受得了。

  使用线程池主要为了解决一下几个问题:

  通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。

  对线程进行一些维护和管理,比如定时开始,周期执行,并发数控制等等。

  2、线程池参数什么意思?

  比如去火车站买票, 有10个售票窗口, 但只有5个窗口对外开放. 那么对外开放的5个窗口称为核心线程数, 而最大线程数是10个窗口.如果5个窗口都被占用, 那么后来的人就必须在后面排队, 但后来售票厅人越来越多, 已经人满为患, 就类似于线程队列已满.这时候火车站站长下令, 把剩下的5个窗口也打开, 也就是目前已经有10个窗口同时运行. 后来又来了一批人,10个窗口也处理不过来了, 而且售票厅人已经满了, 这时候站长就下令封锁入口,不允许其他人再进来, 这就是线程异常处理策略.而线程存活时间指的是, 允许售票员休息的最长时间, 以此限制售票员偷懒的行为.

  3、讲一讲线程池中的threadpoolexecutor,每个参数干什么用的?

  Executor是一个接口,跟线程池有关的基本都要跟他打交道。ThreadPoolExecutor的关系

   
 

  Executor接口很简单,只有一个execute方法。

  ExecutorService是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。

  AbstractExecutorService是一个抽象类。ThreadPoolExecutor就是实现了这个类。

  ThreadPoolExecutor的参数:

  

  corePoolSize

  核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。

  maximumPoolSize

  线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。

  keepAliveTime

  非核心线程的闲置超时时间,超过这个时间就会被回收。

  unit

  指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。

  workQueue

  线程池中的任务队列.

  常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。

  threadFactory

  线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法

  

  通过线程工厂可以对线程的一些属性进行定制。

  默认的工厂:

  

  RejectedExecutionHandler

  RejectedExecutionHandler也是一个接口,只有一个方法:

  public interface RejectedExecutionHandler {

   void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);

  }

  当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法。

  4、说一下线程池内部使用规则

  线程池的线程执行规则跟任务队列有很大的关系。

  下面都假设任务队列没有大小限制:

  如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。

  如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeque的时候,超过核心线程数量的任务会放在任务队列中排队。

  如果线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。

  如果线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是LinkedBlockingDeque并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。

  如果线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。

  任务队列大小有限时

  当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常。

  SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常。

  看代码 ThreadPoolExecutorTest1-7

  5、用过AtomicInteger吗?怎么用的?

  AtomicInteger是int类型的原子操作类,对于全局变量的数值类型操作 num++,若没有加synchronized关键字则是线程不安全的,num++解析为num=num+1,明显,这个操作不具备原子性,多线程时必然会出现问题

  看代码

  要是换成volatile修饰count变量呢?

  volatile修饰的变量能够在线程间保持可见性,能被多个线程同时读但是又能保证只被单线程写,并且不会读取到过期值(由java内存模型中的happen-before原则决定的)volatile修饰字段的写入操作总是优先于读操作,即使多个线程同时修改volatile变量字段,总能保证获取到最新的值

  看代码

  atomicinteger常用方法:

  

  6、用过threadlocal吗?怎么用的?

  早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

  ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。

  其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

  ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

  ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

  

  

   假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

  这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。

  那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。

  到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:

  

  

   这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。

  那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

  但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

  

  从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  每个Thread线程内部都有一个Map。

  Map里面存储线程本地对象(key)和线程的变量副本(value)

  但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

  所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

  ThreadLocal类提供如下几个核心方法:

  

  l get()方法用于获取当前线程的副本变量值。

  l set()方法用于保存当前线程的副本变量值。

  l initialValue()为当前线程初始副本变量值。

  l remove()方法移除当前前程的副本变量值。

  用法 : 见代码 ThreadLocalTest1

  7、程序、进程、线程的区别是什么? 举个现实的例子说明。

   程序(Program):是一个指令的集合。程序不能独立执行,只有被加载到内存中,系统为它分配资源后才能执行。

   进程(Process):如上所述,一个执行中的程序称为进程。

   进程是系统分配资源的独立单位,每个进程占有特定的地址空间。

   程序是进程的静态文本描述,进程是程序在系统内顺序执行的动态活动。

   线程(Thread):是进程的“单一的连续控制流程“。

   线程是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单位,也被称为轻量级的进程。

   线程不能独立存在,必须依附于某个进程。一个进程可以包括多个并行的线程,一个线程肯定属于一个进程。Java虚拟机允许应用程序并发地执行多个线程。

   举例:如一个车间是一个程序,一个正在进行生产任务的车间是一个进程,车间内每个从事不同工作的工人是一个线程。

  8、【上机】Java中通过哪些方式创建多线程类? 分别使用代码说明。并调用之。

  l 继承Thread类创建线程

  l 实现Runnable接口创建线程

  l 实现Callable接口通过FutureTask包装器来创建Thread线程

  l 使用ExecutorService、Callable、Future实现有返回结果的线程

  见代码ThreadCreateTest1-4

  9、Thread类有没有实现Runnable接口?

   有实现。

  

  10、当调用一个线程对象的start方法后,线程马上进入运行状态吗?

   不是,只是进入就绪(可运行)状态,等待分配CPU时间片。一旦得到CPU时间片,即进入运行状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值