java线程实现及线程池的使用

Java线程实现

线程把处理器的调度和资源分配分开,是cpu的最小调度单位。多个线程可以共享进程的内存资源,又可以独立调度。java线程关键方法都是通过高效的本地方法实现的。Java线程的主要实现方式有三种:内核实现、用户实现、内核用户混合实现。

1.内核实现

内核线程就是由内核调度、映射的线程。支持多线程的内核称为多线程内核。这种线程,所有操作都需要系统调度,需要在内核态和用户态切换,系统调用代价比较高。

2.用户实现

这种线程建立在用户空间,在用户态中建立、同步、销毁,不需要内核操作。这种操作非常快速且消耗低,可以支持更多的线程。但是线程阻塞和处理器的分配等功能在不借助内核态的情况下实现起来非常难,所以很少单独使用。

3.内核用户混合实现

最后是上面两种方式的结合,通过用户线程实现线程的创建、切换、析构等,而通过内核提供的轻量级进程实现线程的调度及处理器的映射。

JDK采用的是第二种方式即内核实现,每一个java线程都会对应内核提供的一条轻量级进程。

Java线程调度

线程调度就是分配处理器使用权的过程,主流的调度方式有两种:协同式线程调度和抢占式线程调度。
协同式线程调度中线程的执行时间由线程自身控制,线程执行完后,要主动通知系统切换线程。这种方式实现起来比较简单,且不存在线程同步问题。但是由于线程自身控制切换操作,若某个线程出现问题,可能会导致系统的崩溃。
抢占式线程调度中线程的调度由系统控制,这样就可以避免某个线程挂掉而导致整个系统崩溃。我们的jdk线程就是采用的这种调度方式,系统运行起来会更加的稳定。
当然我们可以通过设置线程的优先级来提高某些线程的执行几率,但是这种方式存在很大的不确定性。因为线程优先级的实现依赖于具体的操作系统平台,不同的平台优先级实现不同,可能会导致java中不同线程优先级在一些平台上却是按相同优先级进行调度的,另外操作系统还可能根据某些策略来忽略线程优先级,所以线程在cpu中的具体调度策略和执行顺序是不可知的,我们不能想当然的臆测线程的执行逻辑。

java线程生命周期

Java线程主要存在5中状态:新建(new)、运行(runnable)、无限期等待(waitting)、有限期等待(timed waiting)、阻塞(blocked)、结束(terminated)。
1.新建:创建后尚未启动的线程。
2.运行:正在执行及等待cpu时间片的线程。
3.无限期等待:不会被分配时间片,等待唤醒的线程。主要包括:使用了Object.wait()、Thread.join()、LockSupport.park()等无timeout参数方法的线程。
4.有期限等待:这种状态的线程也不会被分配时间片,但是在一定时间后系统会自动唤醒它们。主要包括:使用了Thread.sleep()及上面3中几个带timeout参数方法的线程。
5.结束:已经终止的线程。

线程池的优点

由于java线程是通过内核中的轻量级进程实现的,线程创建和销毁都需要切换到内核态,线程生命周期开销非常高。同时新建线程也会导致请求延迟一会才能被处理。另外由于每个线程都会分配一些独立的内存空间,若创建过多的线程会增加内存的占用,同时大量空闲的线程持有对象强引用,会给垃圾回收带来很大的压力,大量的线程竞争cpu资源也会产生很大的性能开销,降低程序的执行速度。在后端服务中经常会出现某些rpc接口的延迟抖动会导致整个服务所有接口性能下降,主要就是因为:依赖的外部接口抖动延迟响应时间变长,请求接口的线程阻塞同时大量请求重试,这时大量新线程被创建,cpu频繁进行用户态内核切换及大量线程争用cpu,导致服务性能逐步下降。线程池的出现非常好的解决了上面的问题,现在代码中已经很少能见到直接new Thread的操作了,有这种操作的程序猿要么是扫地圣僧,要么就是删库跑路的狠人了,哈哈哈哈。

线程及线程池使用注意点

1.尽量避免使用守护线程

Jvm在正常关闭时,会先并行执行关闭钩子及所有已提交和执行中的普通线程,然后去处理定义了finalize方法的对象,做好这些后就会直接结束运行,不会管是否有是正在执行的守护线程,若我们在自定义的守护线程中进行了业务操作或IO操作之类的,就可能造成意外的业务错误。

2.避免改变线程优先级

jvm中的线程优先级只能作为线程调度的参考,线程并不一定按优先级高低顺序执行,这是因为jvm中线程优先级是通过映射系统调度优先级实现的,依赖于特定的平台,而不同平台实现的调度优先级不同,因此两个不同优先级的线程可能被映射成相同的调度优先级。除此之外使用优先级还可能会导致某些线程一直无法获取cpu的调度,进而导致线程的饥饿问题。

3.依赖性任务可能导致线程的饥饿死锁

在线程池中,如果任务依赖于其他任务,并且依赖的任务也在同一线程池中执行,那么便可能产生死锁。当依赖的任务被拒绝或者一直停留在工作队列中,那么任务就会一直阻塞并一直占用线程,队列中任务也获取不到这个线程,就会产生死锁,这种死锁被称为线程饥饿死锁。

4.线程池中的任务应该是同类型的独立任务

计算密集型任务一定不能和IO密集型共用同一个线程池。道理其实很简单,我们举个例子:我们有两个线程并行执行,其中一个需要9毫秒,而另一个需要1毫秒,当我们采用串行执行时任务执行所需时间为10毫秒,而当我并行执行时任务执行所需时间为9毫秒,线程的切换可能还需要一些时间(假设2毫秒),这样算下来抛除线程切换造成的cpu资源浪费,结果并行时间反而还没有串行快,吃力不讨好啊。实际上计算密集型和IO密集型任务不但应该使用不同的线程池,连线程池大小的配置策略也是大不相同,小伙伴们要注意下。由此我们可以进一步推出:执行时间较长的任务不能和执行时较短的任务共用一个线程池,执行时间较长的任务不仅可能造成线程阻塞,也会增加执行时间较短任务的响应时间,甚至当长时间任务的qps大于线程池中的线程数量时,可能会出现所有线程都在执行长时间任务的现象,严重影响服务的性能。总而言之,线程池中的任务应该是同类型的独立任务,并且我们需要根据任务类型去合理配置线程池的线程数量。

发布了477 篇原创文章 · 获赞 588 · 访问量 267万+

猜你喜欢

转载自blog.csdn.net/qq_15037231/article/details/103341331