多线程使用场景:
主要是提升性能,降低延迟,提高吞吐量。
最佳线程数原则:将硬件的性能发挥到极致。
最佳线程数 = CPU核数 * [1+(i/o耗时 / cpu 耗时)]
数据库连接池通过线程封闭技术,保证一个 Connection一旦被一个线程获取之后,在这个线程关闭connnection之前的这段时间里,不会再分配给其他线程,从而保证了connection不会有并发问题。
栈溢出原因:
因为每调用一个方法就会在栈上创建一个栈帧,方法调用结束后就会弹出该栈帧,而栈的大小不是无限的,所以递归调用次数过多的话就会导致栈溢出。而递归调用的特点是每递归一次,就要创建一个新的栈帧,而且还要保留之前的环境(栈帧),直到遇到结束条件。所以递归调用一定要明确好结束条件,不要出现死循环,而且要避免栈太深。
解决方法:
1. 简单粗暴,不要使用递归,使用循环替代。缺点:代码逻辑不够清晰;
2. 限制递归次数;
3. 使用尾递归,尾递归是指在方法返回时只调用自己本身,且不能包含表达式。编译器或解释器会把尾递归做优化,使递归方法不论调用多少次,都只占用一个栈帧,所以不会出现栈溢出。然鹅,Java没有尾递归优化。
引起线程上下文切换的原因:
1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;
2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务;
3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
4. 用户代码挂起当前任务,让出CPU时间;
5. 硬件中断;
Object中 notify()和notifyAll()中,除非经过深思熟虑,否则尽量使用notifyAll()。
使用notify()需要满足3个条件:
1.所有等待线程拥有相同的等待条件。
2.所有等待线程被唤醒后,执行相同的操作。
3.只需要唤醒一个线程。
线程的5个状态:
初始状态:指的是线程已经被创建,但是还不允许分配 CPU 执行。
运行状态:指的是线程可以分配 CPU 执行。当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程。
休眠状态:运行状态的线程如果调用一个阻塞的API或者等待某个事件。
终止状态:线程执行完成或者发生异常。
BLOCKED、WAITING、TIMED_WAITING 可以理解为线程导致休眠状态的三种原因。
1.runnable 与blocked状态转换:
synchronized修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从runnable转换到blocked状态。
2.runnable与waiting状态转换:
第一种场景,获得synchronized隐式锁的线程,调用无参的object.wait()方法。
第二种场景,调用无参的thread.join()方法。
第三种场景,调用LockSupport.park()方法。调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从runnable转换到waiting。
3.runnable与time_waiting的状态转换。
一。调用带超时参数的 Thread.sleep(long millis)方法。
二。获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法。
三。调用带超市参数的Thread.join()方法。
四。调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法。
五。调用带超时参数的LockSupport.parkUntil(long deadline)方法。
stop()和interrupt()方法的主要区别:
stop()方法会真的杀死线程不给喘息的机会,如果线程有reentrantLock锁,被stop的线程不会自动调用ReentrantLock的unlock()方法释放锁。累似的方法还有 suspend()和resume()方法。
interupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt的线程是如何收到通知的呢?一种是异常,一种是主动检车。
当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。
如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。
maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
keepAliveTime & unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了。
workQueue:工作队列,和上面示例代码的工作队列同义。
threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
DiscardPolicy:直接丢弃任务,没有任何异常抛出。
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
不建议使用 Executors 的最重要的原因是:Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
线程池和普通的池化资源有很大不同,线程池实际上是生产者 - 消费者模式的一种实现。
CompletableFuture:
对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决.
CompletionService:
当需要批量提交异步任务的时候建议你使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。除此之外,CompletionService 能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。
CompletionService 的实现类 ExecutorCompletionService,需要你自己创建线程池,虽看上去有些啰嗦,但好处是你可以让多个 ExecutorCompletionService 的线程池隔离,这种隔离性能避免几个特别耗时的务拖垮整个应用的风险。
Fork/Join:
分治任务模型可分为两个阶段:一个阶段是任务分解,也就是将任务迭代地分解为子任务,直至子任务可以直接计算出结果;
另一个阶段是结果合并,即逐层合并子任务的执行结果,直至获得最终结果。下图是一个简化的分治任务模型图,你可以对照着理解。
网友评论