发布 : 2020-01-24 浏览 :

title: 线程

多线程与线程优化CPU核心数与线程数的关系多核心多线程 Simultaneous Multithreading.简称SMT核心数和线程数CPU的时间片轮转机制进程和线程的概念并行和并发Java的线程线程池使用线程池带来的好处线程池工作的策略饱和策略阻塞队列常用阻塞队列(使用最多的应该是LinkedBlockingQueue)合理配置线程池AsyncTask


多线程与线程优化


CPU核心数与线程数的关系


多核心



  • 多核心也就是单个芯片含有多个处理器可以同时工作也叫CMP(Chip Multiprocessor),依靠多个CPU同时并行运行计算。


多线程 Simultaneous Multithreading.简称SMT



  • 多线程通过复制处理器的结构状态,让同一个处理器上的多个线程同步执行并共享处理器的执行资源。


核心数和线程数


一般情况他们是1:1的关系,但是Intel引入了超线程技术后,可以达到1:2的关系


CPU的时间片轮转机制


CPU的时间片轮转机制让我们可以在不受CPU核心数的限制去创建线程,时间片轮转调度是一种古老、最简单、最公平且使用最广的算法,又称为RR调度。每个进程被分配一个时间段,称为它的时间片也就是这个进程能够运行的时间



  • 百度百科的解答


  • 如果在时间片结束的时候进程的执行还没有结束,那么CPU将剥夺它的运行权限,并分配给另外一个进程;如果时间片结束前进程提前结束或者阻塞,CPU也就将时间片分给其他进程。调度程序会维护一张进程列表,同时将时间片结束的进程排到任务队列的末尾


  • 时间片乱转调度中唯一有趣的点是时间片的长度,从一个进程切换到另一个进程是需要耗费时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。所以时间片和进程切换(上下文切换)时间直接影响了效率,时间片太短那么上下文的切换就会频繁,效率就会下降,时间片设置太长也会出现并发事件无法及时响应,通常时间片设置为100ms比较合理。所以CPU不死机的情况下能够被Kill掉其实也是因为时间片轮转机制。



进程和线程的概念



  • 进程


    进程是操作系统分配资源的最小单位,这里的资源就包括了内存、CPU、磁盘IO等。一个进程至少会包含一条线程,线程间共享进程的所有资源。


  • 线程


    线程是CPU调度的最小单元,必须依赖进程存活,线程自己不拥有系统资源,只拥有一点在运行时必不可少的资源(如程序计数器、一组寄存器和栈),但是可以与同进程的其他线程共享资源。



并行和并发


并行和并发的最大区别就在于并行是同时执行,而并发是交替执行的。所以在谈论并发的时候一般会加上单位时间,比如某服务器1s内的并发量是10000,某高速公路每小时可以通过100000辆汽车等



  • 高并发编程带来的意义

  • 充分利用CPU的资源

  • 加快用户的响应时间

  • 代码模块化,异步化,简单化

  • 并发编程的注意事项

  • 线程之间的安全性 同时操作同一份数据

  • 线程之间的死循环 线程间的同步使用锁容易造成死锁

  • 线程太多将资源耗尽造成死机 比如JVM每开一条线程都需要为这条线程分配1M的内存空间,系统的资源是有限的

  • 并发编程的实现方法

  • 互斥同步(阻塞同步)如synchronize reentrantlock等

  • 非阻塞同步 乐观锁

    • 比如CAS指令,CAS指令需要三个操作数,分别是内存位置,旧的预期值,新值。CAS执行是当且仅当内存值符合旧预期值时,处理器用新值更新内存值。并且需要处理器支持CAS的原子操作。JAVA提供了UnSafe类,但是用户无法使用,除非反射。Atomic包下面包含了一些。这里带来了一个ABA问题。ABA实际不大影响,如果要解决可以改用互斥锁

  • 无同步方案 编写可重入的代码以及使用线程本地存储(ThreadLocal、ThreadLocalMap等)

  • CAS存在的问题

  • ABA问题

  • 循环事件长开销大,当然CPU对自旋锁会有优化

  • 只能保证一个共享变量的原子操作,当然在JDK1.5开始,我们可以使用AtomicReference类来保证引用对象之间的原子性,将多个变量放在一个对象里来进行CAS操作。


Java的线程


Java的线程也就是API提供的Thread类,它依赖一个Runnable接口,当然如果需要获取线程运行的结果也可以用Callable和FutureTask配合来使用。



  • 线程的状态切换

  • 新建(New) 创建后尚未启动的线程

  • 运行(Runnable)处于此状态的线程有可能正在执行,也可能正在等待CPU为它分配执行时间

  • 无限期等待(Waiting)处于这种状态的线程不会被分配CPU执行时间,需要等待其他线程对它唤醒,比如调用了以下函数

    • 未设置Timeout参数的Object.wait()方法

    • 未设置Timeout参数的Thread.join()方法

    • LockSupport.park()方法

  • 限期等待(Timed Waiting)处于这种状态的线程也不会被分配CPU执行时间,不过不需要等待被其他线程显示的唤醒,在一定时间之后会有系统自动唤醒。比如调用了以下方法:

    • Thread.sleep()

    • 设置了Timeout的Object.wait()

    • 设置了Timeout的Thread.join()

    • LockSupport.parkNanos()

    • LockSupport.parkUntil()

  • 阻塞(Blocking)线程被阻塞了,阻塞状态与等待状态的区别是阻塞状态在等待获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。而等待状态则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态

  • 结束(Terminated)已终止线程的状态

  • 一些线程方法

  • yield:Thread类方法,是当前线程让出CPU,但是让出的时间没有办法设定,也不会释放锁,而且不一定能成功


线程池


使用线程池带来的好处



  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度:任务不再需要等待线程创建就能立即执行。

  • 提高线程的可管理型:如果无限制的创建线程,可能消耗大量的系统资源,同时无法统一管理,使用线程池可以进行统一分配、调优和监控。


线程池主要提供了ThreadPoolExecutor,当然Executors下面也提供了一些各种用途的线程或者线程池,ThreadPoolExecutor类的初始化参数里面包含了核心线程数、最大线程数、KeepAlive时间、阻塞队列BlockingQueue、RejectedExecutionHandler(这里忽略了ThreadFactory参数)。


线程池工作的策略



  • 少于核心线程数创建核心线程执行

  • 核心线程都在忙则加入到阻塞队列

  • 阻塞队列满了则创建非核心线程(非核心线程数=参数的最大线程数-参数的核心线程数)

  • 非核心线程满了还不够交给RejectedExecutionHandler来处理(这里也叫饱和策略)


饱和策略


线程池一共提供了4中策略



  • AbortPolicy:直接抛出异常,这是默认使用的策略

  • CallerRunsPolicy:用调用者所在的线程来执行任务

  • DiscardolderPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务

  • DiscardPolicy:直接丢弃任务

  • 也可以根据自己的场景实现RejectedExecutionHandler接口来自己定义饱和策略


阻塞队列


队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时称为空队列。因为队列最早插入的元素最早出队,队列又称为先进先出(FIFO)线性表。


阻塞队列



  • 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,知道队列不满。

  • 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。


使用方法



  • 插入方法

  • add 抛异常, offer 返回特殊值, put 一直阻塞

  • 移除方法

  • remove 抛异常, poll 返回特殊值, take 一直阻塞


常用阻塞队列(使用最多的应该是LinkedBlockingQueue



  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列

  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列

  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列

  • SynchronousQueue:一个不存储元素的阻塞队列(这个队列不存储元素,和线程池配合使用如Executors.cachedThreadPool使用的就是它,由于不存储元素所以每来一个任务没有线程接收都新建一条线程去执行。当然也适合在生产者和消费者中使用)

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列

  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列


有界和无界的区别在于是否设定容量,这里的无界是相对的,不可能无止境的添加元素


合理配置线程池


配置线程池之前必须首先分析任务特性,可以从以下角度分析:



  • 任务的性质:CPU密集型任务、IO密集型任务或者混合型任务

  • 任务的优先级

  • 任务的执行时间

  • 任务的依赖性


性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务线程并不会一直在执行任务,应该配置尽可能多的线程,如2*Ncpu。混合型的任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这昂个任务执行时间相差太大,则没必要进行分解,比如网络请求需要消耗500ms,而计算需要花费10s,这样就没必要拆解了。


AsyncTask


作为Android提供的两个线程间通信的机制(另一个是Handler),用的比较少,讲讲坑吧!



  • AsyncTask跟随Android的版本变动较大,比如他的线程池的corePollSize一会是核心数+1,一会直接改成1

  • 虽然内部只有一个线程池,但是却默认创建了一个SerialExecutor,看名字就知道是串行排队执行的,而且其实只是内部维护了一个任务队列,还是使用的线程池的线程去执行任务,坑的就是默认execute使用的就是SerialExecutor,需要调用一个函数才能修改默认采用线程池来执行。

  • 内部还是通过Handler来切换线程到主线程

本文作者 : ShunHe
原文链接 : https://puppiesmeat.github.io/passages/thread/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
留下足迹