从Linux内核的角度来讲,并无线程这个概念。Linux把全部的线程都当作进程来实现,内核没有为线程准备特别的调度算法和特别的数据结构。线程仅仅被视为一个与其余进程共享某些资源的进程。因此,在内核看来,它就是一个普通的进程。linux
在Windows或Solaris等操做系统的实现中,它们都提供了专门支持线程的机制(lightweight processes
)。git
传统的fork()系统调用直接把全部资源复制给新建立的进程,效率十分低下,由于拷贝的数据也许并不须要。github
Linux的fork()使用写时拷贝实现。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。算法
只有在须要写入的时候,数据才会被复制,在此以前,只是以只读方式共享。这种优化能够避免拷贝大量根本就不会被使用的数据(地址空间经常包含几十M的数据)。segmentfault
所以,Linux建立进程和线程的区别就是共享的地址空间、文件系统资源、文件描述符、信号处理程序等这些不一样。网络
如下是StackOverflow上的一个答案:数据结构
即,在Linux
下,进程使用fork()
建立,线程使用pthread_create()
建立;fork()
和pthread_create()
都是经过clone()
函数实现,只是传递的参数不一样,即共享的资源不一样。(Linux
是经过NPTL
实现POSIX Thread
规范,即经过轻量级进程实现POSIX Thread
,使以前在Unix
上的库、软件能够平稳的迁移到Linux
上)多线程
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\n"); return; }
tid
是新建立线程的IDattr
是咱们须要设置的线程属性thread_entry_point
是会被新建立线程调用的函数指针arg_to_entrypoint
是会被传递给thread_entry_point
的参数thread_entry_point
所指向的函数就是Thread对象的run方法。框架
带返回值的线程是咱们在实践中更经常使用的。
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
最多见的竞态条件类型就是“先检查后执行”(Check-Then-Act
)操做,即经过一个可能失效的观测结果来决定下一步的动做。
使用“”先检查后执行“的一种常见状况就是延迟初始化:
public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if (instance == null) { instance = new ExpensiveObject(); } return instance; } }
不要这么作。
在prod
环境中,为每一个任务分配一个线程
的方法存在严重的缺陷,尤为是当须要建立大量的线程时:
JVM
的启动参数、操做系统对线程的限制,若是超出这些限制,极可能会抛出OutOfMemoryError
异常。Executor
基于生产者-消费者模式,提交任务的操做至关于生产者,执行任务的线程则至关于消费者。
线程池的构造函数以下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); }
corePoolSize
:核心线程数,当线程池的线程数小于corePoolSize
,直接建立新的线程corePoolSize
可是小于maximumPoolSize
:若是任务队列还未满, 则会将此任务插入到任务队列末尾;若是此时任务队列已满, 则会建立新的线程来执行此任务。maximumPoolSize
:若是任务队列还未满, 则会将此任务插入到任务队列末尾;若是此时任务队列已满, 则会由RejectedExecutionHandler
处理。keepAliveTime
:当咱们的线程池中的线程数大于corePoolSize
时, 若是此时有线程处于空闲(Idle)状态超过指定的时间(keepAliveTime
), 那么线程池会将此线程销毁。工做队列(WorkQueue
)是一个BlockingQueue
, 它是用于存放那些已经提交的, 可是尚未空余线程来执行的任务。
常见的工做队列有一下几种:
Direct handoffs
)Unbounded queues
)Bounded queues
)在生产环境中,禁止使用无界队列,由于当队列中堆积的任务太多时,会消耗大量内存,最后OOM
;一般都是设定固定大小的有界队列,当线程池已满,队列也满的状况下,直接将新提交的任务拒绝,抛RejectedExecutionException
出来,本质上这是对服务自身的一种保护机制,当服务已经没有资源来处理新提交的任务,因直接将其拒绝。
在服务化的背景下,咱们的框架通常都会集成全链路追踪
的功能,用来串联整个调用链,主要是记录TraceId
和SpanId
;TraceId
和SpanId
通常都记录在ThreadLocal
中,对业务方来讲是透明的。
当在同一个线程中同步RPC调用的时候,不会存在问题;但若是咱们使用线程池作客户端异步调用时,就会致使Trace
信息的丢失,根本缘由是Trace
信息没法从主线程的ThreadLocal
传递到线程池的ThreadLocal
中。
对于这个痛点,阿里开源的transmittable-thread-local
解决了这个问题,实现其实不难,能够阅读一下源码:
https://github.com/alibaba/transmittable-thread-local
提高性能意味着用更少的资源作更多的事情。“资源”的含义很广,例如CPU
时钟周期、内存、网络带宽、磁盘空间等其余资源。当操做性能因为某种特定的资源而受到限制时,咱们一般将该操做称为资源密集型的操做,例如,CPU密集型、IO密集型等。
使用多线程理论上能够提高服务的总体性能,但与单线程相比,使用多线程会引入额外的性能开销。包括:线程之间的协调(例如加锁、触发信号以及内存同步),增长的上下文切换,线程的建立和销毁,以及线程的调度等。若是过分地使用线程,其性能可能甚至比实现相同功能的串行程序更差。
从性能监视的角度来看,CPU须要尽量保持忙碌状态。若是程序是计算密集型的,那么能够经过增长处理器来提高性能。但若是程序没法使CPU保持忙碌状态,那增长更多的处理器也是无济于事的。
可伸缩性是指:当增长计算资源时(例如CPU、内存、存储容量、IO带宽),程序的吞吐量或者处理能力能响应的增长。
咱们熟悉的三层模型,即程序中的表现层、业务逻辑层和持久层是彼此独立,而且可能由不一样的服务来处理,这很好地说明了提升伸缩性一般会形成性能损失。若是把表现层、业务逻辑层和持久层都融合到某个单体应用中,在负载不高的时候,其性能确定要高于将应用程序分为多层的性能。这种单体应用避免了在不一样层次之间传递任务时存在的网络延迟,减小了不少开销。
然而、当单体应用达到自身处理能力的极限时,会遇到一个严重问题:提高它的处理能力很是困难,即没法水平扩展。
大多数并发程序都是由一系列的并行工做和串行工做组成的。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:用户进程执行时间百分比
以上。