天天看点

JVM、多线程面试题GC如何判断对象可以被回收线程的生命周期,线程有哪些状态sleep()、wait()、join()、yield()的区别说说你对线程安全的理解Thread、Runable的区别说说你对守护线程的理解ThreadLocal的原理的使用场景ThreadLocal内存泄漏问题,如何避免并发、并行、串行的区别并发的三大特性为什么用线程池?解释下线程池参数?线程池处理流程线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程线程池中线程复用原理

JVM、多线程面试题

  • GC如何判断对象可以被回收
  • 线程的生命周期,线程有哪些状态
  • sleep()、wait()、join()、yield()的区别
  • 说说你对线程安全的理解
  • Thread、Runable的区别
  • 说说你对守护线程的理解
    • 守护线程的作用是什么?
  • ThreadLocal的原理的使用场景
  • ThreadLocal内存泄漏问题,如何避免
  • 并发、并行、串行的区别
  • 并发的三大特性
    • 原子性(CAS)
    • 可见性(volatile)
    • 有序性
  • 为什么用线程池?解释下线程池参数?
  • 线程池处理流程
  • 线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程
  • 线程池中线程复用原理

GC如何判断对象可以被回收

引用计数法:每个对象有一个引用计算属性,新增一个引用时计数加1,引用释放时计数减一,计数为0时可以回收。

可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。(目前JAVA中使用的就是这种方式)

GC Roots的对象有:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

    可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize。

线程的生命周期,线程有哪些状态

1.线程通常有五种状态:创建,就绪,运行,阻塞和死亡状态

2.阻塞的情况又分为三种:

  • 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
  • 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/o请求时,JVM会把线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

新建状态(New):就创建一个线程对象。

就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

sleep()、wait()、join()、yield()的区别

1.锁池:所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。

2.等待池:当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池中。

(1)、sleep是Thread类的静态本地方法,wait()则是Object类的本地方法

(2)、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中

(3)、sleep一般用于当前线程休眠,或者轮询暂停操作,wait()则多用于线程之间的通信

(4)、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)

(5)、 sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字

(6)、sleep会让出cpu执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行

join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join,那么线程B会进入到阻塞队列,直到线程A结束或中断线程

说说你对线程安全的理解

线程安全指的是内存安全,堆是共享内存的,可以被所有线程访问

堆:是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是会内存泄漏。

栈:是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显示的分配和解放。

  • 目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
  • 在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

Thread、Runable的区别

Thread和Runable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runable。

说说你对守护线程的理解

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆。

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它依赖整个进程而运行;只要其他线程全部结束了,守护线程就会中断了。

守护线程的作用是什么?

举例:GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做了,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:(1)、来为其它线程提供服务支持的情况;(2)、或者在任何情况下,程序结束时,这个线程必须正常立刻关闭,就可以作为守护线程来使用;反之,如果立刻关闭会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比如说,数据库录入或者更新,这些操作都是不能中断的。

特点:

  • thread.setDaemon(true)(设置这个线程是守护线程)必须在thread.start()之前设置,否则会报一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置成守护线程。
  • 在Daemon线程中产生的新线程也是Daemon的;
  • 守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发送中断
  • Java自动的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用Java的线程池。

ThreadLocal的原理的使用场景

  • 每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它的存储本线程中所有ThreadLocal对象及其对应的值。
  • ThreadLocalMap由一个个Entry对象构成。
  • Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和Object构成。由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用,该key就会白垃圾收集器回收。
  • 当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
  • get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
  • 由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

    使用场景:

    1.在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

    2.线程间数据隔离

    3.进行事务操作,用于存储线程事务信息

    4.数据库连接,Session会话管理

ThreadLocal内存泄漏问题,如何避免

  • 内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光。
  • 不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。
  • 强引用:使用最普遍的强引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,JAVA虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收这种对象。

    如果想要取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

    弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在JAVA中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法:

  • 每次使用完ThreadLocal都要调用它的remove()方法清除数据。
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

并发、并行、串行的区别

串行在时间上不可能发送重叠,前一个任务没结束,下个任务就只能等着

并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行

并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行。

并发的三大特性

原子性(CAS)

原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。

如何保证原子性:

  • 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
  • 通过 Lock 接口保障原子性。
  • 通过 Atomic 类型保障原子性。

可见性(volatile)

当多线程访问同一个变量时,一个线程修改了这个变量值,其他线程能够立即看得到修改的值。

volatile 变量和普通变量区别:

  • 普通变量与 volatile 变量的区别是 volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

如何保证可见性:

  • 通过 volatile 关键字标记内存屏障保证可见性。
  • 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
  • 通过 Lock 接口保障可见性。
  • 通过 Atomic 类型保障可见性。
  • 通过 final 关键字保障可见性

有序性

虚拟机在进行编译时,程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。

如何保证有序性

  • 通过 volatile 可以保障有序性。

为什么用线程池?解释下线程池参数?

  • 降低资源消耗,提高线程的利用率,降低创建和销毁线程的消耗。
  • 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
  • 提高线程的可管理性;线程是稀缺资源,使用线程可以统一分配调优监控。

线程池参数

参数: ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler)

  1. corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
  2. maximumPoolSize代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数
  3. keepAliveTime、unit表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会销毁,但是除核心线程之外的线程在空闲一定的时间则会被消除,我们就可以通过keepAliveTime来设置空闲时间
  4. workQueue用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  5. threadFactory实际上是一个线程工厂,用来执行任务。
  6. handler是任务拒绝策略,有两种情况:
    1. 当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还没有执行完的任务正在执行,但是由于线程池已经关闭,我们再向线程池提交任务就会遭到拒绝。
    2. 另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这也是拒绝的。

线程池处理流程

线程池执行任务,先判断核心线程池是否已满,未满,创建核心线程执行;未满,然后再判断任务队列是否已满,未满,将任务放入到队列种;已满,看最大线程数是否达到,未达到,创建临时线程执行;如果达到,根据拒绝策略处理任务。

线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使线程进入wait状态,释放cpu资源。

在创建新线程的时候,是要获取全局锁,这个时候其它的线程就得阻塞,影响了整体效率。

线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制