Interview ...

线程(一)

2019-02-20  本文已影响4人  FrankerSung

First And MOST Important

\color{red}{如果有错误还请在评论区提出🙏🙏 建议看下本文末尾的参考地址,有些具体的点应该比我总结的更为详尽,希望大家从中受益并尊重原创。🙂}

1. 线程的生命周期

生命周期

2. 线程池的原理,为什么要创建线程池?

目的:
创建线程需要分配本地方法栈、虚拟机栈、程序计数器等内存空间;
销毁线程需要回收所分配的资源;
创建线程池可以减少两部分的消耗。

优点:
周期任务,定时执行等与时间相关的功能;
复用线程、控制最大并发数目;
隔离线程环境。

3. 什么是线程安全,如何实现线程安全

关于线程安全,可以说一千个人有一千个哈姆雷特。
针对类来说,当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
针对数据来说,数据应该是正确的。
针对逻辑来说,多线程下的行为是人所预支,会按照开发者的预期进行执行。

实现线程安全的方法

4. JDK创建线程池有哪几个核心参数? 如何合理配置线程池的大小?

1.核心和最大的线程容量
TPE会根据corePoolSize和maximumPoolSize自动调整线程池的大小。
当一个任务提交到线程池里:
如果当前运行的线程池大小小于corePoolSize,则不管是否有空闲线程,都会创建一个新的线程来运行任务。
如果当前运行的线程池容量大于corePoolSize,小于maximumPoolSize,只有在队列满的时候才会创建新的线程。
设置core=maximum即创建一个固定大小的线程池。设置maximum为Integer.MAX_VALUE即创建一个可无限创建线程边界的线程池。
一般情况下,在构造TPE时就会设置好了这两个参数,但是也可以通过set方法动态设置。

2.线程存活时间
如果线程池的大小超过了corePoolSize,如果超过这个大小的线程在指定时间内是空闲的,则会被终止掉。
默认只终止超过corePoolSize的线程,但是可以通过设置allowCoreThreadTimeOut来关闭超时的core线程。
注:慎重的设置这个参数,设置不当会失去创建线程池所带来的性能提升。

3.线程等待队列
任意BlockingQueue都可用于传输和保存提交的任务。使用这个队列与线程池大小进行交互:
如果当前运行的线程小于corePoolSize,则Executor会选择创建一个新线程而不是入队。
如果当前运行的线程大于或等于corePoolSize,则Executor会选择将任务入队而不是创建一个新线程。
如果无法将任务入队,则创建新的线程---除非线程池大小已经达到了maximumPoolSize,这种情况下任务会被拒绝。

使用队列有三种通用策略:
无界队列。使用无界队列(如无预定义容量的LinkedBlockingQueue),当所有corePoolSize线程都工作时,新任务都入队等待。这样,创建的线程永远不会超过corePoolSize(也就是说设置maximumPoolSize的值不会起任何作用)。当每一个任务都完全独立于其他任务时,即任务都不影响其他任务的执行时,适合选择无界队列;比如在Web页服务器中。无界队列可用于平滑处理突然激增的请求,当请求以超过队列所能处理的平均值连续到达时,无界队列能增加自己的容量。
有界队列。当使用有限的maximumPoolSizes时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能比较难调整和控制。需要权衡设置有界队列和maximumPoolSizes的大小:使用大容量的队列和小的线程池可降低CPU的使用率、操作系统资源和上下文切换开销,但是可能导致吞吐量降低。如果任务频繁阻塞(比如阻塞在I/O操作上),则系统可能会为超过你许可的更多线程安排时间。使用小容量的队列一般需要较大容量的线程池,会使CPU利用率较高,但是可能遇到不能接受的调度开销,也会使吞吐量降低。
直接提交。工作队列默认选择SynchronousQueue,它将任务直接提交给线程而不hold它们。如果不存在可立即运行任务的线程,则尝试入队将会失败,因此会创建一个新的线程。这个策略可以避免在处理可能具有内部依赖的请求时出现锁。直接提交通常要求无限大的maximumPoolSizes以避免拒绝新提交的任务。当任务以超过所能处理的平均数连续到达时,此策略允许无界线程持续增加。

注:合理使用三种队列,注意其中的参数设置,防止踩坑。

4.拒绝任务的策略
当Executor已经关闭/有界的最大线程值和队列容量都已经饱和时,提交的新任务将被拒绝。
在以上两种情况下, execute方法都将调用RejectedExecutionHandler的RejectedExecutionHandler.rejectedExecution方法。
这个类预定义了四种处理策略:
默认使用ThreadPoolExecutor.AbortPolicy,提交任务被拒绝时将抛出运行时异常:RejectedExecutionException。
使用ThreadPoolExecutor.CallerRunsPolicy,线程池调用提交该任务的线程去执行。这个策略提供了简单的反馈控制机制,能够减缓新任务的提交速度。
使用ThreadPoolExecutor.DiscardPolicy,不能执行的任务将被丢弃掉。
使用ThreadPoolExecutor.DiscardOldestPolicy,如果执行程序尚未关闭,则位于队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
我们也可以定义和使用其他形式的RejectedExecutionHandler类,但是当策略仅用于特定容量或排队策略时要非常非常小心。

5. ThreadLocal什么时候会OOM?ThreadLocal为什么会内存泄漏?

结构图

ThreadLocal实现原理:每个Thread维护一个ThreadLocalMap,这个map的key是ThreadLocal实例本身,value 是需要存储的对象,也就是说ThreadLocal本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
注意:关系比较乱,最好看看源码自己画下图,加深理解

内存泄露的点:ThreadLocalMap的生命周期与Thread相同,线程未结束时,线程里会维护着ThreadLocalMap,这个map对应的key--threadLocal尽管回收掉了,但是这个map却还是存在的,所以到value的引用还在,不能回收导致泄露内存。

private static final Boolean FIX_OOM = false;
...
for (int i = 0; i < Integer.MAX_VALUE; i++) {

    executorService.execute(() -> {
        threadLocal.set(new ThreadLocalOutOfMemory().getData());
        Thread t = Thread.currentThread();
        log.info(t.getName());
        if (FIX_OOM) {
            // getMap(Thread.currentThread()).remove(this) 从Map里移除当前threadLocal对应的对象
            threadLocal.remove();
        }
    });
    try {
        Thread.sleep(500L);
    } catch (InterruptedException ignore) {
        Thread.currentThread().interrupt();
    }
}

如何解决呢?上述代码里也写了,调用threadLocal的remove方法即可。

6. ThreadLocal的使用场景

ThreadLocal提供了线程本地的实例。它与普通变量的区别在于每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

ThreadLocal 适用于,具体如web应用里的session等:

7. 内存可见性、原子性、synchronized、synchronized锁粒度、volatile

Java内存模型参考文章---JVM(一)

可见性:一个线程修改共享变量,其他线程能够及时看到

原子性:操作不可再分
如x++操作就不是原子性操作,x++分为3个操作:
读取变量count的当前值 --> count和1相加 --> 将增加后的值赋给count。

volatile、synchronized两者的区别联系

volatile synchronized
只能变量级别 可以在变量、方法、类级别
变量需要从主存中读取 锁定当前变量,具有排他性,只有当前线程可以访问该变量
保证变量可见性、不保证原子性 保证变量可见性和原子性
volatile变量不会被编译器优化,禁止指令重排序 变量可以被编译器优化

synchronized锁粒度:

修饰方法 修饰的方法是普通的成员方法 对象锁
修饰的方法是静态方法 类锁
修饰一个代码块 锁this 对象锁
锁一个类的class对象 类锁
锁普通对象,对象是静态对象 类锁
锁普通对象,对象为非静态对象 对象锁

参考资料
http://www.cnblogs.com/onlywujun/p/3524675.html

上一篇下一篇

猜你喜欢

热点阅读