资讯专栏INFORMATION COLUMN

对Java多线程的一些理解

Nekron / 2465人阅读

摘要:线程仅仅被视为一个与其他进程共享某些资源的进程。稳定性可创建的线程的数量上存在限制,包括的启动参数操作系统对线程的限制,如果超出这些限制,很可能会抛出异常。若是密集型程序产生大量的线程切换,将会降低系统的吞吐量。

OS中的进程、线程

进程:即处于执行期的程序,且包含其他资源,如打开的文件、挂起的信号、内核内部数据、处理器状态、内核地址空间、一个或多个执行的线程、数据段。

线程:进程中的活动对象,内核调度的对象不是进程而是线程;传统Unix系统一个进程只包含一个线程。

线程在Linux中的实现

从Linux内核的角度来说,并没有线程这个概念。Linux把所有的线程都当做进程来实现,内核没有为线程准备特别的调度算法和特别的数据结构。线程仅仅被视为一个与其他进程共享某些资源的进程。所以,在内核看来,它就是一个普通的进程。

在Windows或Solaris等操作系统的实现中,它们都提供了专门支持线程的机制(lightweight processes)。

写时拷贝

传统的fork()系统调用直接把所有资源复制给新创建的进程,效率十分低下,因为拷贝的数据也许并不需要。

Linux的fork()使用写时拷贝实现。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。

只有在需要写入的时候,数据才会被复制,在此之前,只是以只读方式共享。这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间常常包含几十M的数据)。

因此,Linux创建进程和线程的区别就是共享的地址空间、文件系统资源、文件描述符、信号处理程序等这些不同。

以下是StackOverflow上的一个答案:

即,在Linux下,进程使用fork()创建,线程使用pthread_create()创建;fork()pthread_create()都是通过clone()函数实现,只是传递的参数不同,即共享的资源不同。(Linux是通过NPTL实现POSIX Thread规范,即通过轻量级进程实现POSIX Thread,使之前在Unix上的库、软件可以平稳的迁移到Linux上)

Java线程如何映射到OS线程

JVM在linux平台上创建线程,需要使用pthread 接口。pthread是POSIX标准的一部分它定义了创建和管理线程的C语言接口。Linux提供了pthread的实现:

pthread_t tid;
if (pthread_create(&tid, &attr, thread_entry_point, arg_to_entrypoint))
{
      fprintf(stderr, "Error creating thread
");
      return;
}

tid是新创建线程的ID

attr是我们需要设置的线程属性

thread_entry_point是会被新创建线程调用的函数指针

arg_to_entrypoint是会被传递给thread_entry_point的参数

thread_entry_point所指向的函数就是Thread对象的run方法。

无返回值线程和带返回值的线程

无返回值:一种是直接继承Thread,另一种是实现Runnable接口

带返回值:通过Callable和Future实现

带返回值的线程是我们在实践中更常用的。

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

最常见的竞态条件类型就是“先检查后执行”(Check-Then-Act)操作,即通过一个可能失效的观测结果来决定下一步的动作。

使用“”先检查后执行“的一种常见情况就是延迟初始化:

public class LazyInitRace {
    private ExpensiveObject instance = null;
    
    public ExpensiveObject getInstance() {
        if (instance == null) {
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

不要这么做。

Executor框架 使用裸线程的缺点

prod环境中,为每个任务分配一个线程的方法存在严重的缺陷,尤其是当需要创建大量的线程时:

线程生命周期的开销非常高:线程的创建与销毁并不是没有代价的。

资源消耗:会消耗内存和CPU,大量的线程竞争CPU资源将产生性能开销。如果你已经拥有足够多的线程使所有CPU处于忙碌状态,那么创建更多的线程反而会降低性能。

稳定性:可创建的线程的数量上存在限制,包括JVM的启动参数、操作系统对线程的限制,如果超出这些限制,很可能会抛出OutOfMemoryError异常。

Executor基本原理

Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。

线程池的构造函数如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
线程池大小

corePoolSize:核心线程数,当线程池的线程数小于corePoolSize,直接创建新的线程

线程数大于corePoolSize但是小于maximumPoolSize:如果任务队列还未满, 则会将此任务插入到任务队列末尾;如果此时任务队列已满, 则会创建新的线程来执行此任务。

线程数等于maximumPoolSize:如果任务队列还未满, 则会将此任务插入到任务队列末尾;如果此时任务队列已满, 则会由RejectedExecutionHandler处理。

keep-alive

keepAliveTime:当我们的线程池中的线程数大于corePoolSize时, 如果此时有线程处于空闲(Idle)状态超过指定的时间(keepAliveTime), 那么线程池会将此线程销毁。

工作队列

工作队列(WorkQueue)是一个BlockingQueue, 它是用于存放那些已经提交的, 但是还没有空余线程来执行的任务。

常见的工作队列有一下几种:

直接切换(Direct handoffs)

无界队列(Unbounded queues)

有界队列(Bounded queues)

在生产环境中,禁止使用无界队列,因为当队列中堆积的任务太多时,会消耗大量内存,最后OOM;通常都是设定固定大小的有界队列,当线程池已满,队列也满的情况下,直接将新提交的任务拒绝,抛RejectedExecutionException 出来,本质上这是对服务自身的一种保护机制,当服务已经没有资源来处理新提交的任务,因直接将其拒绝。

Java原生线程池在生产环境中的问题

在服务化的背景下,我们的框架一般都会集成全链路追踪的功能,用来串联整个调用链,主要是记录TraceIdSpanIdTraceIdSpanId一般都记录在ThreadLocal中,对业务方来说是透明的。

当在同一个线程中同步RPC调用的时候,不会存在问题;但如果我们使用线程池做客户端异步调用时,就会导致Trace信息的丢失,根本原因是Trace信息无法从主线程的ThreadLocal传递到线程池的ThreadLocal中。

对于这个痛点,阿里开源的transmittable-thread-local解决了这个问题,实现其实不难,可以阅读一下源码:

https://github.com/alibaba/transmittable-thread-local
性能与伸缩性 对性能的思考

提升性能意味着用更少的资源做更多的事情。“资源”的含义很广,例如CPU时钟周期、内存、网络带宽、磁盘空间等其他资源。当操作性能由于某种特定的资源而受到限制时,我们通常将该操作称为资源密集型的操作,例如,CPU密集型、IO密集型等。

使用多线程理论上可以提升服务的整体性能,但与单线程相比,使用多线程会引入额外的性能开销。包括:线程之间的协调(例如加锁、触发信号以及内存同步),增加的上下文切换,线程的创建和销毁,以及线程的调度等。如果过度地使用线程,其性能可能甚至比实现相同功能的串行程序更差。

从性能监视的角度来看,CPU需要尽可能保持忙碌状态。如果程序是计算密集型的,那么可以通过增加处理器来提升性能。但如果程序无法使CPU保持忙碌状态,那增加更多的处理器也是无济于事的。

可伸缩性

可伸缩性是指:当增加计算资源时(例如CPU、内存、存储容量、IO带宽),程序的吞吐量或者处理能力能响应的增加。

我们熟悉的三层模型,即程序中的表现层、业务逻辑层和持久层是彼此独立,并且可能由不同的服务来处理,这很好地说明了提高伸缩性通常会造成性能损失。如果把表现层、业务逻辑层和持久层都融合到某个单体应用中,在负载不高的时候,其性能肯定要高于将应用程序分为多层的性能。这种单体应用避免了在不同层次之间传递任务时存在的网络延迟,减少了很多开销。

然而、当单体应用达到自身处理能力的极限时,会遇到一个严重问题:提升它的处理能力非常困难,即无法水平扩展。

Amdahl定律

大多数并发程序都是由一系列的并行工作和串行工作组成的。Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器上,最高的加速比为:

当N趋近于无穷大时,最大的加速比趋近于1/F。因此,如果程序中有50%的计算需要串行执行,那么最高的加速比只能是2。

上下文切换

线程调度会导致上下文切换,而上下文切换是会产生开销的。若是CPU密集型程序产生大量的线程切换,将会降低系统的吞吐量。

UNIX系统的vmstat命令能够报告上下文切换次数以及在内核中执行时间的所占比例等信息。如果内核占用率较高(超过10%),那么通常表示调度活动发生得很频繁,这很可能是由I/O或者锁竞争导致的阻塞引起的。

>> vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 3235932 238256 3202776    0    0     0    11    7    4  1  0 99  0  0
 
 cs:每秒上下文切换次数
 sy:内核系统进程执行时间百分比
 us:用户进程执行时间百分比

以上。

原文链接

https://segmentfault.com/a/11...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71272.html

相关文章

  • 学习Java线程一些总结

    摘要:多线程环境下的一些问题安全性问题在没有正确同步的情况下,多线程环境下程序可能得出错误的结果。一些相关概念竞争条件多线程的环境下,程序执行的结果取决于线程交替执行的方式。而线程的交替操作顺序是不可预测的,如此程序执行的结果也是不可预测的。 入口 Java多线程的应用复杂性之如jvm有限的几个内存方面的操作和规范,就像无数纷繁复杂的应用逻辑建立在有限的指令集上。 如何写出线程安全的程序,有...

    coolpail 评论0 收藏0
  • 金三银四,2019大厂Android高级工程师面试题整理

    摘要:原文地址游客前言金三银四,很多同学心里大概都准备着年后找工作或者跳槽。最近有很多同学都在交流群里求大厂面试题。 最近整理了一波面试题,包括安卓JAVA方面的,目前大厂还是以安卓源码,算法,以及数据结构为主,有一些中小型公司也会问到混合开发的知识,至于我为什么倾向于混合开发,我的一句话就是走上编程之路,将来你要学不仅仅是这些,丰富自己方能与世接轨,做好全栈的装备。 原文地址:游客kutd...

    tracymac7 评论0 收藏0
  • 金三银四,2019大厂Android高级工程师面试题整理

    摘要:原文地址游客前言金三银四,很多同学心里大概都准备着年后找工作或者跳槽。最近有很多同学都在交流群里求大厂面试题。 最近整理了一波面试题,包括安卓JAVA方面的,目前大厂还是以安卓源码,算法,以及数据结构为主,有一些中小型公司也会问到混合开发的知识,至于我为什么倾向于混合开发,我的一句话就是走上编程之路,将来你要学不仅仅是这些,丰富自己方能与世接轨,做好全栈的装备。 原文地址:游客kutd...

    沈建明 评论0 收藏0
  • jvm原理

    摘要:在之前,它是一个备受争议的关键字,因为在程序中使用它往往收集器理解和原理分析简称,是后提供的面向大内存区数到数多核系统的收集器,能够实现软停顿目标收集并且具有高吞吐量具有更可预测的停顿时间。 35 个 Java 代码性能优化总结 优化代码可以减小代码的体积,提高代码运行的效率。 从 JVM 内存模型谈线程安全 小白哥带你打通任督二脉 Java使用读写锁替代同步锁 应用情景 前一阵有个做...

    lufficc 评论0 收藏0
  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    Lucky_Boy 评论0 收藏0
  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    LuDongWei 评论0 收藏0

发表评论

0条评论

Nekron

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<