Java 并发常见知识点&面试题3/16

请简要描述线程与进程的关系,区别及优缺点?

image.png 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈 和 本地方法栈

总结:  线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反

为什么程序计数器虚拟机栈本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈:  每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈:  和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。  在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

说说线程的生命周期和状态?

image.png 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换

什么是上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出cpu,比如调用了wait,sleep等
  • 时间片已经用完
  • 调用阻塞类型的系统中断,比如请求IO,线程被阻塞
  • 被终止和结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

什么是线程死锁?如何避免死锁?

多个线程同时被阻塞,或者是他们中的一个或者多个全部在等在某一个资源被释放,由于线程无期限的被阻塞,所以程序不可能正常停止

学过操作系统的朋友都知道产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

说说 sleep() 方法和 wait() 方法区别和共同点?

  • 最主要的区别是sleep()方法没有释放锁,而wait()释放了锁
  • 两者都用于线程的交互通信,sleep()被用于暂停执行
  • wait调用后不会自动苏醒,需要别的线程调用同一个对象上的notify()方法

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

1.synchronized 关键字

#1.1.说一说自己对于synchronized关键字理解

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

1.3. 构造方法可以使用 synchronized 关键字修饰么?

构造方法本身就属于线程安全的,不存在同步的构造方法一说。 synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

1.5. 谈谈 synchronized 和 ReentrantLock 的区别

#1.5.1. 两者都是可重入锁

可重入锁是自己可以再次获取自己的内部锁,比如一个线程获得了某一个对象的锁,此时这个对象还没有被释放,当他再次想要获取这个锁的时候还是可以获取的,如果不是的话就会造成死锁

1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能

  • 等待可中断
  • 可实现公平锁
  • 可实现选择性通知

2. volatile 关键字

4.1. 为什么要用线程池?

减少每次获取资源的消耗,提高对资源的利用率。

使用线程池的好处

-降低资源消耗 -提高响应速度 -提高线程可管理性

#4.2. 实现 Runnable 接口和 Callable 接口的区别

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。

4.3. 执行 execute()方法和 submit()方法的区别是什么呢?

1.execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功与否

2.submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future可以判断任务是否执行成功

4.4. 如何创建线程池

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

三种类型的ThreadPoolExecutor

  • FixedThreadPool:该方法返回一个固定线程数量的线程池,该线程池中的数据始终保持不变,当有新任务提交,线程池如有空闲线程,则立刻执行,如果没有暂存任务对列
  • SingleThreadExecutor:返回一个只有一个线程的线程池先入先出
  • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

4.5.2 ThreadPoolExecutor 饱和策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy  抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy  调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy  不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy  此策略将丢弃最早的未处理的任务请求。

猜你喜欢

转载自juejin.im/post/7075575374804942878