文章目录

  • 1.底层机制
  • 1.1 Java线程
  • 内核线程实现
  • 用户线程实现
  • 混合实现
  • Java线程实现
  • 1.2 线程调度
  • 协同式线程调度
  • 抢占式调度
  • 1.3 线程状态转换
  • 新建(New)
  • 运行(Runnable)
  • 无限期等待(Waiting)
  • 限期等待(Timed Waiting)
  • 阻塞(Blocked)
  • 2.面临的问题
  • 2.1 上下文切换问题
  • 上下文切换
  • 减少上下文切换
  • 相关命令
  • 减少大量WAITING线程,来减少上下文切换
  • 2.2 死锁
  • 原因
  • 避免死锁
  • 3.资源限制


1.底层机制

1.1 Java线程

并发并不一定依赖多线程,但Java里谈论并发大多数都与线程脱不开关系。
线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU调度的基本单位)。
Thread类的所有关键方法都声明了native的,意味着这个方法没有使用或无法使用平台无关的手段来实现,也有可能是为了执行效率。

内核线程实现

内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核。
程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP),即通常意义上的线程*。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。*轻量级进程与内核线程之间1:1关系称为一对一的线程模型。
内核线程保证了每个轻量级进程都成为一个独立的调度单元,即时有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程的继续工作。
局限:基于内核线程实现,因此各线程操作等需要系统调用,系统调用代价高,需要在用户态和内核态来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,如内核线程的栈空间,因此一个系统支持轻量级进程的数量是有限的。

用户线程实现

广义上,内核线程以外,就是用户线程。轻量级也算用户线程,但轻量级进程的实现始终是建立在内核上>的,许多操作都要进行系统调度,效率会受到限制。
狭义上,用户线程指完全建立在用户空间的线程库上。这种线程不需要切换内核态,效率非常高且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
用户线程优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都是需要用户程序自己处理。阻塞处理等问题的解决十分困难,甚至不可能完成。所以使用用户线程会非常复杂。

混合实现

内核线程与用户线程混合实现。可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低整个进程被完全阻塞的风险。用户线程与轻量级进程比例是N:M。

Java线程实现

JDK1.2之前,绿色线程——用户线程。JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。 Solaris同时支持一对一和多对多。

1.2 线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度。

协同式线程调度

协同式线程调度,线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

抢占式调度

抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
Java线程调度就是抢占式调度。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

1.3 线程状态转换

Java定义了5种线程状态,在任意一个点一个线程只能有且只有其中一种状态。无限等待和等待可以算在一起。所以共五种。

新建(New)

创建后尚未启动的线程。

运行(Runnable)

Runnable包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能等待CPU为它分配执行时间。线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。

无限期等待(Waiting)

该状态下线程不会被分配CPU执行时间,要等待被其他线程显式唤醒。如没有设置timeout的object.wait()方法和Thread.join()方法,以及LockSupport.park()方法。

限期等待(Timed Waiting)

不会被分配CPU执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后会由系统自动唤醒。如Thread.sleep(),设置了timeout的object.wait()和thread.join(),LockSupport.parkNanos()以及LockSupport.parkUntil()方法。

阻塞(Blocked)

线程被阻塞了。与等待状态的区别是:阻塞在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待则在等待一段时间,或唤醒动作的发生。在等待进入同步区域时,线程将进入这种状态。
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,,即无限期等待
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。即限期等待。
这种情况下,五个状态分为 新建,就绪,运行,阻塞,终止,

2.面临的问题

2.1 上下文切换问题

上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回来这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。上下文切换消耗并发执行的速度。

减少上下文切换

无锁并发编程:多线程竞争锁时,会引起上下文的切换,所以多线程处理数据时可以通过一些方法避免使用锁,如将数据ID按照hash取模分段,不同的线程处理不同段的数据
CAS算法:Atomic包使用CAS算法更新数据
使用最少的线程:避免创建不需要的线程
协程:在单线程中完成任务调度。并在单线程中维持多个任务间的切换。

相关命令

【vmstat 1 】CS ------------- Content Switch上下文切换次数
java@ubuntu:~/Downloads/idea-IU-191.6707.61/bin$ vmstat 1
procs -----------memory---------- —swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 538112 249280 33612 647412 0 2 25 34 78 5 0 0 99 0 0
0 0 538112 249156 33612 647496 0 0 84 0 371 645 0 1 99 0 0
0 0 538112 249032 33612 647496 0 0 0 0 463 720 1 0 99 0 0
0 0 538112 249032 33612 647496 0 0 0 40 386 645 0 0 99 0 0
0 0 538112 249032 33612 647496 0 0 0 0 350 604 0 0 99 0 0

减少大量WAITING线程,来减少上下文切换
  1. jstack 59728 > dump
    导出线程信息
  2. grep java.lang.Thread.State dump |awk ‘{print $2}’|sort|uniq -c
    8 RUNNABLE
    2 WAITING
    统计线程状态
  3. 在配置中减少线程数

2.2 死锁

原因

• 互斥条件:线程要求对所分配的资源进行排他性控制,即在一段时间内某 资源仅为一个进程所占有.此时若有其他进程请求该资源.则请求进程只能等待.
• 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放).
• 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放.
• 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

避免死锁

• 避免一个线程同时获取多个锁
• 避免一个线程在锁内同时占有多个资源
• 尝试使用定时锁
• 对于数据库,加锁和解锁必须放在一个数据库连接里

3.资源限制

资源限制是在进行并发编程时,程序的执行速度受限于计算机的硬件资源或软件资源。可能导致并行部分仍然串行执行,增加上下文切换和资源调度反而更慢。硬件资源限制可以采取集群并行执行程序。软件资源可以考虑资源池的复用。根据不同的资源限制调整程序的并发度。