JAVA并发编程(九)之多线程未必快

多线程在某些情况下可以提升程序的性能,但是不合理的使用多线程并不一定能提升程序的执行效率

一、多线程并不一定比串行快

这段代码窜行的执行速度,不比并发的慢

二、JAVA线程与LINUX的线程关系

线程实现在用户空间下(ULT):线程在用户空间下实现时,操作系统对线程的存在一无所知,操作系统只能看到进程,而不能看到线程,所有的线程都是在用户空间实现;在操作系统看来,每一个进程只有一个线程
优点:就是线程的调度只是在用户态,减少了操作系统从内核态到用户态的切换开销,就是线程的调度只是在用户态,减少了操作系统从内核态到用户态的切换开销。
缺点:
1、程序员需要自己实现线程的数据结构、创建销毁和调度维护,就是自己实现线程的调度
2、这种模式最致命的缺点也是由于操作系统不知道线程的存在,因此当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作


线程实现在操作系统内核中(KLT):

内核线程就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel);程序员直接使用操作系统中已经实现的线程,而线程的创建、销毁、调度和维护,都是靠操作系统(内核)来实现,程序员只需要使用系统调用,而不需要自己设计线程的调度算法和并发并发安全问题。

  1. LWP:在计算机操作系统中,轻量级进程(LWP)是一种实现多任务的方法,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息,一般来说,一个进程代表程序的一个实例,而LWP代表程序的执行线程;LWP的一个重要作用是提供了一个用户级线程实现的中间系统。LWP可以通过系统调用获得内核提供的服务,因此,当一个用户级线程运行时,只需要将它连接到一个LWP上便可以具有内核支持线程的所有属性。
  2. KLT:即内核线程Kernel Thread,是“内核分身”。每一个KLT对应到进程P中的某一个轻量级进程LWP(也即线程),期间要经过用户态、内核态的切换,并在Thread Scheduler 下反应到处理器CPU上
  3. Thread Scheduler:操作系统的线程调度器

这种线程实现的缺陷:在程序面上使用内核线程,必然在操作系统上多次来回切换用户态及内核态;另外,因为是一对一的线程模型,LWP的支持数是有限的。

JDK1.2之前,程序员们为JVM开发了自己的一个线程调度内核,而到操作系统层面就是用户空间内的线程实现。而到了JDK1.2及以后,JVM选择了更加稳健且方便使用的操作系统原生的线程模型,通过系统调用,将程序的线程交给了操作系统内核进行调度。也就是说目前JVM使用的线程调度方式取决与操作系统的线程模型,这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。程序一般不直接使用该内核线程,而是使用其高级接口,即轻量级进程(LWP)

java可以这样获取CPU的核数:Runtime.getRuntime().availableProcessors();

三、线程切换

单核cpu的并发执行:由于单核CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢? 操作系统的设计者 巧妙地利用了时间片轮转的方式,时间片轮询将引发一个新的技术问题,就是线程的上下文切换。
线程上下文切换:CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行,这段过程就叫做上下文切换。

  1. 切出: 一个线程被剥夺处理器的使用权而被暂停运行,将当前线程的运行信息保存起来
  2. 切入: 一个线程被系统选中占用处理器开始或继续运行,CPU需要将改线程切出时的数据加载到寄存器与程序计数器

线程信息:每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。
寄存器: 是 CPU 内部的数量较少但是速度很快的存储区域,主要用来提高计算机程序运行的速度。
程序计数器PC:是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

上下文切换的消耗:

  1. 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
  2. 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。

线程让出cpu的情况:

  1. 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上、获取锁失败
  3. 当前运行线程结束,即运行完run()方法里面的任务

引起线程上下文切换的因素

  1. 当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
  2. 中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
  3. 用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
  4. 多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换

四、如何减少上下文切换

  • 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

参考:https://www.cnblogs.com/dennyzhangdd/p/7486233.html   https://www.jianshu.com/p/7c980955627e

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值