logo头像

Always believe youself.

并发编程-1

进程和线程

进程和线程

image.png

二者对比

  • 进程基本上互相独立的,而线程是存在于进程内的,是进程的一个子集。
  • 进程拥有共享的资源,如内存空间,功其内部线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为IPC
    • 不同计算机通信,需要网络,并有共同的协议,如 HTTP
  • 线程通信相对简单,因为他们共享进程内的内存,一个例子是多线程可以访问一个共享变量
  • 线程更轻量,线程上下文切换成本一般比进程上下文切换低。

并行和并发

并发:(单核)线程轮流使用CPU, concurrent。 微观串行,宏观并行。

并行:多核都可以调度运行线程,这个时候可以是并行的。

引用的描述:

  • 并行(concurrent) 是同一时间应对(dealing with)多件事情的能力。
  • 并行(parallel) 是同一时间动手(doing)多件事情的能力。

方法调用:

  • 同步:需要等待结果返回,才能继续运行
  • 异步,不需要等待结果返回,就能继续运行

同步在多线程中还有另一个意思,就是让多个线程步调一致。

image.png

线程

创建和运行线程

  • 创建 Thread 用匿名内部类的写法,覆盖其中的 run 方法,执行代码放入到 run 方法内。 然后用 start 进行启动。
  • 使用 Runnable (接口,就一个 run 方法)配合 Thread,Thread 代表线程,Runnable 代表可运行的任务。
    • lambda 进行精简。Thread t = new Thread((log.info("xxxxxxxxx"))->{},"tName");
  • FutureTask 配合Thread :FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。可以线程间通信
  • 线程池的方式

Thread 和 Runnable 的关系

  • 方法 1 是线程和任务合并了一起,方法二是把线程和任务分开了
  • 用 Runnable 更容易与线程池高级 API 进行配合
  • 用 Runnable 让任务类 脱离了Thread 继承体系更灵活。

查看线程和进程的方法

windows

  • 任务管理器
  • tasklist 查看进程
  • taskill 杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID) 的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID) 的所有线程

java

  • jps 查看所有 java 进程
  • jstack 查看某个进程下的所有线程,查看某一刻的,快照信息,更详细的信息。
  • jconsole 查看某个进程所有的线程,内存,cpu,类等情况。

线程 API

线程状态

原理

栈与栈帧

jvm 中由堆,栈,方法区组成,其中栈内存是给线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈帧,对应着正在执行的那个方法

image.png

线程上下文切换

image.png

image.png

线程上下文切换会保存前线程的所有信息(局部变量,栈帧,返回地址等等),再去执行下一个线程。

线程中常见方法

run&start

image.png

image.png

  • run : 调用run 的话,方式还是在主线程中调用的,未实现异步等效果,与线程无关。
  • start:启动线程必须用start。无法多次调用,

    线程状态: start 之前是 NEW ,之后是 RUNNABLE

sleep&yield

  • sleep :

    • 调用sleep 会让当前线程从Running 进入 Time Waiting 状态
    • 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
    • 睡眠结束后的线程未必立刻得到执行
    • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
  • yield(让出CPU使用权)

    • 调用 yield 会让当前线程 Running 进入 Runnable 状态,然后调度执行其他优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。
    • 具体的实现依赖于操作系统的任务调度器。
程序优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但cpu 空闲时,优先级几乎没用。

sleep 实现

image.png

join 方法

等待线程运行结束 t.join() 等待 t 线程的结束,运行到这会卡住。

获取多个其他线程的结果,只需调用其他多个线程的 join 即可。

有时效的 join 。 t.join(n), n 等待时间。 小于实际则 直接返回,大于直接,则执行完就返回,不会多等待。

interrupt

打断 sleep, wait, join 的线程,会有打断过的标记。

打断阻塞状态:

  • 打断 sleep 的线程,会清空打断状态,(正常情况,有打断,打断标记是 真, sleep 的打断标记是 false)

打断非阻塞状态:

打断正常运行的线程:

image.png

打断标记的用途,优雅的退出。

利用interrupt 的 设计模式:

两阶段终止模式(Two phase Termination)

在线程 t1 中 如何优雅的终止线程 t2? 【优雅】指的是给 t2 一个料理后事的机会。

image.png

设计思路 & code:

image.png

打断 park 线程:

打断 park 线程,不会清空打断状态。

不推荐的方法

不推荐使用的方法,这些方法已过时,容易破坏代码块,造成线程死锁。

stop()     停止线程运行
suspend()  挂起(暂停)线程运行
resume()   恢复线程运行

主线程与守护线程

默认情况下,Java 进行需要等到所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它的非守护线程运行结束了,即使守护线程的代码还没有执行结束完,也会强制结束。

注意:

垃圾回收器线程就是一种守护线程
Tomcat 中 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完当前请求。

线程的的五种状态

image.png

  • 【初始状态】 仅是在语言层面上创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】 (就绪状态) 指该线程已经被创建,(与操作系统线程关联),可以由CPU 调度执行
  • 【运行状态】 指获取CPU 时间片运行中的状态
    • 当CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞API, 如 BIO 读写文件,这时候线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完成,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要他们一直不唤醒,调度器就一直不会考虑调度他们
  • 【终止状态】 表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态。

线程的的六种状态

image.png

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用 start() 方法之后,注意,Java API 层面的RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】【运行状态】【阻塞状态】 (由于BIO导致的线程阻塞,在Java里面无法区分,仍然认为是可运行的)

  • BLOCKED、 WAITING、 TIMED_WAITTING 都是 Java API 层面对 【阻塞状态】 的细分,后面会在状态转换详述

  • TERMINATED 当线程代码运行结束