进程和线程
进程是程序运行资源分配的最小单位
- 资源:CPU、内存、磁盘IO等
- 进程间相互独立,同一个进程间多个线程共享该进程的全部资源
- 运行一个程序就是启动了一个进程,程序是死的,进程是活的
线程是CPU调度的最小单位,必须依赖于进程而存在
- 线程本身只有很少的系统资源(程序计数器、栈)
- 可以和属于同一个进程的其他线程共享进程的系统资源
CPU核心数和线程数的关系
多核心:单芯片多处理器,将多个处理器集成到一个芯片上,各个处理器执行不同的进程
- 并行处理:多个CPU同时并行的运行程序
多线程:同一个处理器上的多个线程同步执行,并共享处理器的执行资源
- 并发处理:侧重于多个任务交替执行,处理器同一时刻只会有一个线程在执行
核心数、线程数:通常CPU都是多核的,增加核心数的目的就是为了增加线程数
- 操作系统就是通过线程来执行任务的,所以线程线程数越多,处理能力越强
- 正常情况下,核心数:线程数 = 1:1;利用intel的超线程技术可以达到1:2的关系
CPU时间片轮转机制
也称为RR调度
- 每个进程被分配一个时间段(时间片,表示进程允许运行的时间)
- 如果在时间片结束时进程还在运行,该进程的CPU将被剥夺并分配给另一个进程
- 如果进程在时间片结束前阻塞或者完成,立即切换CPU资源给下一个进程
- 调度程序需要做的就是维护一张就绪进程表(FIFO队列),当有进程用完它的时间片后,将它移动到队列末尾
指令寄存器:存放当前正在执行的指令 程序计数器:存放下一条要执行的指令地址
时间片长度
进程之间切换时需要保存和装入寄存器值以及内存映像,更新各种表格和队列,这是需要一定时间的。所以时间片的长度设置非常重要,不能过大或者过小
- 过大,近似变成串行了,可能引起对要求响应时间比较短的交互请求响应变差
- 过小,进程间频繁切换消耗大量时间,降低了CPU的效率
并行和并发
- 并发侧重于多个任务交替执行
- 并行表示同时执行多个任务
临界区
一种可以被多个线程共享的数据,但是同一时刻只能有一个线程使用
阻塞和非阻塞
- 阻塞:一个线程占用临界区资源时,其他需要这个资源的线程必须在临界区等待,导致线程挂起
- 非阻塞:一个线程不能阻止其他线程的执行,所有线程都会尝试不断向前执行
活跃性问题
死锁
- 死锁的必要条件
- 如何避免死锁
饥饿
- 线程由于无法访问它需要的资源而不能执行时,就发生了饥饿
- 最常见引发饥饿的资源就是CPU时钟周期。如果线程的优先级使用不当,或者持有锁时执行一些无法结束的逻辑(无限循环等),也可能发生饥饿
- 多数java应用中,所有线程优先级相同,尽量不要改变线程的优先级,否则可能会发生饥饿问题
活锁
- 两个线程请求资源时,互相“谦让”,主动将资源让给对方,导致没有一个线程能够同时拿到所有资源
- 通常发生在处理事务消息的程序中
- 如果消息处理器不能处理某种特定类型的消息,发生错误回滚事务,并且将其重新放在队列开头,这样虽然处理消息的线程没有阻塞,但是也不能继续执行
- 这个问题的原因是过度的错误恢复代码造成的,将不可修复的问题错误的当作可以修复的问题
- 如果消息处理器不能处理某种特定类型的消息,发生错误回滚事务,并且将其重新放在队列开头,这样虽然处理消息的线程没有阻塞,但是也不能继续执行
- 解决活锁的办法是在重试机制中引入随机性
高并发的好处
- 充分利用CPU资源(多核多线程CPU)
- 减少响应用户的时间
- 代码模块化、异步化、简单化(电商系统拆分订单系统、短信系统、邮件系统等)
使用多线程注意事项
- 线程之间的安全性,注主要针对的是共享资源
- 线程间的死锁
- 线程数量过多导致服务器资源耗尽宕机
JMM
原子性
一个操作是不可中断的,一旦开始,要么成功要么失败,不会被其他线程干扰
基本类型的读写是原子性的,long和double除外【把它们当成2个原子性的32位值】
- 可以使用volatile实现long和double读写的原子性,这是volatile在原子性上的唯一贡献
JVM规范规定
- 实现对普通long与double的读写不要求是原子的(但如果实现为原子操作也OK)
- 实现对volatile long与volatile double的读写必须是原子的(没有选择余地)
可见性
一个线程修改了某个共享变量的值后,其他线程是否能够立即知道这个修改
有序性
单线程或者多线程串行环境下指令重排是没有任何问题的,但是多线程并行时可能会出现问题
指令重排可以提高CPU的处理性能,是必要的
happen-before原则(重排序不会破坏的原则)