面试【JAVA基础】多线程


本次整理的内容以下:java

java基础之线程池.png

一、进程与线程的区别

进程是一个可执行的程序,是系统资源分配的基本单位;线程是进程内相对独立的可执行单元,是操做系统进行任务调度的基本单位。算法

二、进程间的通讯方式

2.一、操做系统内核缓冲区

因为每一个进程都有独立的内存空间,进程之间的数据交换须要经过操做系统内核。须要在操做系统内核中开辟一块缓冲区,进程 A 将须要将数据拷贝到缓冲区中,进程 B 从缓冲区中读取数据。由于共享内存没有互斥访问的功能,需配合信号量进行互斥访问。编程

2.二、管道

管道的实现方式:小程序

  • 父进程建立管道,获得两个描述文件指向管道的两端。
  • 父进程 fork 出子进程,子进程也拥有两个描述文件,指向同一个管道的两端。
  • 父进程关闭读端(fd(0)),子进程关闭写端(fd(1))。父进程往管道里面写,子进程从管道里面读。

管道的特色:
只容许具备血缘关系的进程间通信,只容许单向通信,进程在管道在,进程消失管道消失。管道内部经过环形队列实现。
有名管道(命名管道):
经过文件的方式实现进程间的通讯。容许无血缘关系的进程间的通讯数组

2.三、消息队列

由消息组成的链表,存在系统内核中。克服了信号量传递的信息少,管道只能承载无格式的字符流及缓冲区的大小受限等特色。经过消息类型区分消息。服务器

2.四、信号量

本质是一个计数器,不以传送数据为目的,主要用来保护共享资源,使得资源在一个时刻只有一个进程独享。多线程

2.五、套接字

可用于不一样机器间进程的通讯。
套接字包括 3 个属性:域、类型、 协议。并发

  • 域包括 ip 端口
  • 类型指的是两种通讯机制:流(stream)和数据报(datagram)
  • 协议指 TCP/UDP 底层传输协议

建立 socket 经过 bind 命名绑定端口,listen 建立队列保存未处理的客户端请求,accept 等待客户端的链接,connect 服务端链接客户端 socket,close 关闭服务端客户端的链接。异步

stream 和 datagram 的区别:
stream 能提供有序的、可靠的、双向的、基于链接的字节流(TCP),会有拆包粘包问题。
datagram 是无链接、不可靠、使用固定大小的缓冲区的数据报服务(UDP),由于基于数据报,且有固定的大小,因此不会有拆包粘包问题。socket

详细请参考:进程间的五种通讯方式介绍

三、线程间的通讯方式

共享内存:
Java 采用的就是共享内存,内存共享方式必须经过锁或者 CAS 技术来获取或者修改共享的变量,看起来比较简单,可是锁的使用难度比较大,业务复杂的话还有可能发生死锁。
消息传递:
Actor 模型便是一个异步的、非阻塞的消息传递机制。Akka 是对于 Java 的 Actor 模型库,用于构建高并发、分布式、可容错、事件驱动的基于 JVM 的应用。消息传递方式就是显示的经过发送消息来进行线程间通讯,对于大型复杂的系统,可能优点更足。

详细请参考:Java 内存模型分析

四、多线程的优缺点

优势:
充分利用 cpu 的资源,提升 cpu 的使用率,使程序的运行效率提升。
缺点:
有大量的线程会影响性能,操做系统会在线程之间切换,会增长内存的开销。可能会产生死锁、存在线程之间的并发问题。

五、建立线程的方法

  1. 集成 Thread 类,重写 run 方法,利用 start 启动线程。
  2. 实现 Runable 接口建立线程,重写 run 方法,经过 new Thread 方式建立线程。
  3. 经过 callable 和 futuretask 建立线程,实现 callable 接口,重写 call 方法,使用 future 对象包装 callable 实例,经过 new Thread 方式建立线程。
  4. 经过线程池建立线程。

六、runable 和 callable 区别

  1. runable 是重写 run 方法,callable 重写 call 方法。
  2. runable 没有返回值,callable 有返回值。
  3. callable 中的 call 方法能够抛出异常,runable 中的 run 方法不能向外界抛出异常。
  4. 加入线程池运行 runable 使用 execute 运行,callable 使用 submit 方法。

七、sleep 和 wait 区别

  1. wait 只能在 synchronized 块中调用,属于对象级别的方法,sleep 不须要,属于 Thread 的方法。
  2. 调用 wait 方法时候会释放锁,sleep 不会释放锁。
  3. wait 超时以后线程进入就绪状态,等待获取 cpu 继续执行。

八、yield 和 join 区别

  1. yield 释放 cpu 资源,让线程进入就绪状态,属于 Thread 的静态方法,不会释放锁,只能使同优先级或更高优先级的线程有执行的机会。
  2. join 等待调用 join 方法的线程执行完成以后再继续执行。join 会释放锁和 cpu 的资源,底层是经过 wait 方法实现的。

九、死锁的产生条件

  1. 互斥条件。
  2. 请求与保持条件。
  3. 不可剥夺条件。
  4. 循环等待条件。

详细请参考:并发编程挑战:死锁与上下文切换

十、如何解决死锁

  1. 破坏请求与保持条件
    静态分配,每一个线程开始前就获取须要的全部资源。
    动态分配,每一个线程请求获取资源时自己不占有资源。
  2. 破坏不可剥夺条件
    当一个线程不能获取全部的资源时,进入等待状态,其已经获取的资源被隐式释放,从新加入到系统的资源列表中,可被其余线程使用。
  3. 死锁检测:银行家算法

十一、threadLocal 的实现

  1. ThreadLocal 用于提供线程局部变量在多线程环境下能够保证各个线程里面的变量独立于其余线程里的变量。
  2. 底层使用 ThreadLocalMap 实现,每一个线程都拥有本身的 ThreadLocalMap,内部为继承了 WeakReference 的 Entry 数组,包含的 Key 为 ThreadLocal,值为 Object。

详细请参考:【SharingObjects】ThreadLocal

十二、threadLocal 何时会发生内存泄漏

java.lang.ThreadLocal.ThreadLocalMap.Entry:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                //重点!!!!!
                super(k);
                value = v;
            }
        }

由于 ThreadLocalMap 中的 key 是弱引用,而 key 指向的对象是 threadLocal,一旦把 threadLocal 实例置为 null 以后,没有任何强引用的对象指向 threadLocal 对象,所以 threadLocal 对象会被 Gc 回收,但与之关联的 value 却不能被回收,只有当前线程结束后,对应的 map value 才会被回收。若是当前线程没结束,可能会致使内存泄漏。
如线程池的场景,在线程中将 threadlocal 置为 null,但线程没被销毁且一直不被使用,就可能会致使内存泄漏

在调用 get、set、remove 方法时,会清除线程 map 中全部 key 为 null 的 value。因此在不使用 threadLocal 时调用 remove 移除对应的对象。

1三、线程池

13.一、线程池类结构

ThreadPoolExecutor 继承关系图:
ThreadPoolExecutor.png

13.二、shutDown 和 shutDownNow 的区别、

shutDown 方法执行以后会变成 SHUTDOWN 状态,没法接受新任务,随后等待已提交的任务执行完成。
shutDownNow 方法执行以后变成 STOP 状态,没法接受新任务。并对执行中的线程执行 Thread.interrupt()方法。

  • SHUTDOWN:不接受新任务提交,可是会继续处理等待队列中的任务。
  • STOP:不接受新任务提交,再也不处理等待队列中的任务,中断正在执行任务的线程。
13.三、线程池的参数
  1. CorePoolSize 核心线程数
  2. MaximumPoolSize 最大线程数,线程池容许建立的最大线程数
  3. keepAliveTime 空闲线程的存活时间
  4. wokeQueue 任务队列
  5. handler 饱和策略
  6. threadFactory 用于生成线程。

当任务来时,若是当前的线程数到达核心线程数,会将任务加入阻塞队列中,若是阻塞队列满了以后,会继续建立线程直到线程数量达到最大线程数,若是线程数量已经达到最大线程数量,且任务队列满了以后,会执行拒绝策略。

若是想让核心线程被回收,可使用 allowCoreThreadTimeOut 参数,若是为 false(默认值),核心线程即便在空闲时也保持活动状态。若是 true,核心线程使用 keepAliveTime 来超时等待工做。

13.四、线程池的饱和策略
  1. CallerRunsPolicy:由提交任务的线程本身执行这个任。
  2. AbortPolicy (默认): 直接抛出 RejectExecutionException 异常。
  3. DisCardPolicy:不作处理,抛弃掉当前任务。
  4. DiscardOldestPolicy: 把队列队头的任务直接扔掉,提交当前任务进阻塞队列。
13.五、线程池分类

java.util.concurrent.Executors 类:

  1. newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

生成一个固定大小的线程池,此时核心线程数和最大线程数相等,keepAliveTime = 0 ,任务队列采起 LinkedBlockingQueue 无界队列(也可设置为有界队列)。
适用于为了知足资源管理需求,而须要限制当前线程数量的应用场景,好比负载比较重的服务器。

  1. newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));

生成只有一个线程的线程池,核心线程数与最大线程数都是 1,keepAliveTime = 0,任务队列采起 LinkedBlockingQueue,适用于须要保证顺序地执行各个任务,而且在任意时间点不会有多个线程是活动的应用场景。

  1. newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

核心线程数是 0,最大线程数是 int 最大值,keepaliveTime 为 60 秒,任务队列采起 SynchronousQueue,适用于执行不少的短时间异步任务的小程序,或者是负载较轻的服务器。

  1. newScheduledThreadPool
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

定长的线程池,支持周期性任务,最大线程数是 int 最大值,超时时间为 0,任务队列使用 DelayedWorkQueue,适用于须要多个后台执行周期任务,同时为了知足资源管理需求而须要限制后台线程的数量的应用场景。

13.六、任务执行过程当中出现异常会怎么样?

任务执行失败后,只会影响到当前执行任务的线程,对于整个线程池是没有影响的。

详细请参考:ThreadPoolExecutor 线程池任务执行失败的时候会怎样

13.七、线程池的底层实现
  1. 使用 hashSet 存储 worker
  2. 每一个 woker 控制本身的状态
  3. 执行完任务以后循环获取任务队列中的任务
13.八、重启服务、如何优雅停机关闭线程池

kill -9 pid 操做系统内核级别强行杀死某个进程。
kill -15 pid 发送一个通知,告知应用主动关闭。

ApplicationContext 接受到通知以后,会执行 DisposableBean 中的 destroy 方法。
通常咱们在 destroy 方法中作一些善后逻辑。
调用 shutdown 方法,进行关闭。

13.九、为何使用线程池
  1. 下降资源消耗,减小建立销毁线程的成本。
  2. 提升响应速度。
  3. 提升线程的可管理性,线程的无限制的建立,消耗系统资源,下降系统的稳定性。

相关文章
相关标签/搜索