JAVA面试常见问题之进程和线程篇
1、线程和进程的概念、并行和并发的概念
- 进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
- 并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。
- 并发:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
2、创建线程的方式及实现
创建线程的三种方式及实现代码:
- 继承Thread类创建
public class MyThread extends Thread{//继承Thread类 public void run(){ //重写run方法 } } public class Main { public static void main(String[] args){ new MyThread().start();//创建并启动线程 } }
2. 实现Runnable接口创建
public class MyThread2 implements Runnable {//实现Runnable接口 public void run(){ //重写run方法 } } public class Main { public static void main(String[] args){ //创建并启动线程 MyThread2 myThread=new MyThread2(); Thread thread=new Thread(myThread); thread().start(); //或者 new Thread(new MyThread2()).start(); } }
3. 使用Callable和Futura创建
3、进程间通信的方式
进程间的通信(IPC)常用方式管道(无名管道和命名管道)、消息队列、信号量、共享存储、套接字(Socket)及Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
- 管道:
- 无名管道:管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。它是半双工的,具有固定的读端和写端。只能用于亲缘关系间的进程之间的通信。
- FIFO:也称命名管道,它是一种文件类型。可以在无关的进程之间交换数据。有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
- 消息队列:是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。特点:
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 信号量:是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。特点:
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
- 支持信号量组。
- 共享存储:指两个或多个进程共享一个给定的存储区。特点:
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
- 因为多个进程可以同时操作,所以需要进行同步。
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
- 总结:
- 无名管道:速度慢,容量有限,只有父子进程能通讯
- FIFO:任何进程间都能通讯,但速度慢
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
- 信号量:不能传递复杂消息,只能用来同步
- 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
- 无名管道:速度慢,容量有限,只有父子进程能通讯
4、说说 CountDownLatch、CyclicBarrier 原理和区别
原理: 参考:https://blog.csdn.net/wantflydacheng/article/details/81664035
区别:
5、说说 Semaphore 工作原理,举例说明
Semaphore主要用于控制当前活动线程数目,就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行
6、说说 Exchanger 原理
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的。
Exchanger类提供了两个方法,String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待。
参考:https://blog.csdn.net/carson0408/article/details/79477280
7、ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
- ThreadLocal是本地线程变量,用于保存某个线程的共享变量。方法包含get(),set(),remove(),initialValue()
- 不正当的使用ThreadLocal时候会出现OOM,线程池的一个线程使用完ThreadLocal对象之后,再也不用,由于线程池中的线程不会退出,线程池中的线程的存在,同时ThreadLocal变量也会存在,占用内存!造成OOM溢出!
- 出现OOM的深层次原理 参考:https://mp.weixin.qq.com/s/xqb1kUVtD82JuvlqWGV18w 第二部分 由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。使用线程池,线程不会被销毁,ThreadLocalMap也不会销毁,随着线程数量也来越多,ThreadLocalMap也会越来越多,占得内存越来越大,从而导致出现内存溢出。
8、讲讲线程池的实现原理
一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队 列中有任务了就取出来继续执行。
几个重要参数意义:
- corePoolSize: 规定线程池有几个线程(worker)在运行。
- maximumPoolSize: 当workQueue满了,不能添加任务的时候,这个参数才会生效。规定线程池最多只能有多少个线程(worker)在执行。
- keepAliveTime: 超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并且超过了keepAliveTime设定的时间,就会消亡。
- unit: 生存时间对于的单位
- workQueue: 存放任务的队列
- threadFactory: 创建线程的工厂
- handler: 当workQueue已经满了,并且线程池线程数已经达到maximumPoolSize,将执行拒绝策略。
9、线程池的几种实现方式
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
10、线程的生命周期,状态是如何转移的
生命周期: 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
状态转移:
1. 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
2. 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
3. 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
4. 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
5. 在线程的生命周期当中,线程的各种状态的转换过程
参考:https://www.cnblogs.com/sunddenly/p/4106562.html
另有线程相关面试题:https://mp.weixin.qq.com/s/rW0H5oez1IfMOtaPBwWGoA(可能和上面的有重复)