Java并发编程概览
目录
主要知识
- 多线程基础核心 → Synchronized实现原理 → Volatile实现原理 → JMM和指令重排
- JUC → 原子类与CAS实现原理 → 锁与AQS实现原理→ 并发工具类→ 并发容器→ 阻塞队列→ 线程池
多线程相关概念
线程和进程
- 进程:是指内存中运行的一个应用程序,每个进程都有自己独立的内存空间;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过 程。
- 线程:是进程中的一个执行单元,负责当前进程中任务的执行。一个进程在其执行过程中,会产生很多个线程。
进程与线程区别
- 进程:有独立内存空间,每个进程中的数据空间都是独立的。
- 线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消耗的资源比进程小的多。
并发与并行
- 并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行。
- 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。并行上限取决于CPU核数(CPU时间片内50ms)
注意:并发是一种能力,而并行是一种手段。当我们的系统拥有了并发的能力后,代码如果跑在多核CPU上就可以并行运行。所以咱们会说高并发处理,而不会说高并行处理。并行处理是基于硬件CPU的是固定的,而并发处理的能力是可以通过设计编码进行提高的。
线程上下文切换
一个CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。
- CPU切换线程,会把当前线程的执行位置记录下来,用于下次执行时找到准确位置。
- 线程执行位置的记录与加载过程就叫做上下文切换。
- 线程执行位置记录在程序计数器
上下文切换过程
- 挂起当前任务任务,将这个任务在 CPU 中的状态(上下文)存储于内存中的某处。
- 恢复一个任务,在内存中检索下一个任务的上下文并将在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指定的位置(即跳转到任务被中断时的代码行)。
线程上下文切换会有什么问题呢?
过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延长。因此,合理控制上下文切换次数,可以提高多线程应用的运行效率。(也就是说线程并不是越多越好,要合理的控制线程的数量。)
- 直接消耗:指的是CPU寄存器需要保存和加载,系统调度器的代码需要执行
- 间接消耗:指的是多核的cache之间得共享数据,间接消耗对于程序的影响要看线程工作区操作数据的大小
线程安全问题
什么是线程安全问题
- 多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的预期值也一样,就是线程安全的,反之则是线程不安全。
引发线程安全问题的根本原因:多个线程共享变量
- 如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的
- 如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的
解决线程安全问题,Java给出了各种办法
- 同步机制Synchronized
- Volatile关键字:内存屏障
- 原子类:CAS
- 锁:AQS
- 并发容器
线程并发三大特性
原子性
- 分时复用引起:操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异导致。
- 原子性:一个系列指令代码,要么全执行,要么都不执行,执行过程不能被打断
|
|
可见性
- 为什么会出现不可见问题呢?因为Java内存模型(JMM), CPU缓存引起:CPU增加了缓存,以均衡与内存的速度差异导致。
- 当多个线程访问同一个变量时,一个线程修改了共享变量的值,其他线程能够立即看到
有序性
- 重排序引起:由于编译程序指令重排序优化指令执行次序,使得缓存能够得到更加合理地利用导致。
- 有序性:程序代码按照先后顺序执行
要想多线程程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
线程的一生
常用属性
- 线程名称
- 线程ID:ThreadID = tid
- 线程优先级:Priority
常用方法
- 线程让步:yield()
- 让线程休眠的方法:sleep()
- 等待线程执行终止的方法: join()
- 线程中断interrupt()
- 等待与通知系列函数wait()、notify()、notifyAll()
线程从出生到死亡会出现六种状态
- ①New(新建)、②Runnable(可运行) 、③Terminated(终止)
- ④Blocked(锁阻塞)、⑤Waiting(无限等待)、⑥Timed_Waiting(超时等待)
wait与sleep()的区别
- 主要区别:sleep()方法没有释放锁,wait()方法释放了锁
- 两者都可以暂停线程执行:wait()常用于线程间交互/通信,sleep()用于暂停线程执行
- wait()方法被调用后,需要别的线程调用同一个对象的notify和notifyAll。超时苏醒使用wait(long)方法
- sleep()方法执行完成后,线程会自动苏醒。
多线程源码剖析
Java线程是通过start()方法启动,启动后会执行run()方法Thread究竟是如何执行run()方法呢?
- 线程类被JVM加载时会绑定native方法与对应的C++方法
- Java线程调用start方法:start方法 ==> native state0方法 ==> JVM_StartThread ==> 创建JavaThread::JavaThread线程
- 创建OS线程,并指定OS线程的运行入口:创建JavaThread::JavaThread线程 ==> 创建OS线程os::create_thread ==> 指定OS线程执行入口Java线程的run方法
- 启动OS线程,运行时会调用指定的运行入口run()方法。至此,实现一个的线程运行
- 创建线程的时候使用的是互斥锁MutexLocker操作系统(互斥量),所以说创建线程性能很差