- Java源程序运行步骤(半解释语言 semi-interpreted language)
- Java文件被编译器编译为字节码文件(JDK中的前端编译器:javac.exe)
- 词法分析
- 语法分析
- 语义分析
- 中间代码生成
- 加载字节码文件(ClassLoader 类加载器)
- 将字节码文件解释为汇编指令(JVM中的后端编译器:JIT即时编译,AOT静态编译;JVM中的解释器:java.exe)
- 执行(JRE;它包含了JVM不具有的运行字节码文件所需的Java类库)
- Java文件被编译器编译为字节码文件(JDK中的前端编译器:javac.exe)
- JVM中的所有线程共享堆内存、方法区,但每个线程都有各自的方法栈;PC寄存器(程序计数器)存放下一条待执行指令的地址,JVM会在方法栈中压入一个栈帧存放字节码操作数和局部变量。入栈后,交由JVM解释执行。
- 每个线程都有自己对应的PC寄存器,每个线程只有当前方法(Java方法栈或本地方法栈)在执行
- Java字节码解释器(java.exe)是JVM的一部分,用于解释执行字节码文件
- Java解释器通过改变PC寄存器的值,来明确下一条待执行的字节码指令
- 进程(Process) v.s. 线程(Thread)
- 将程序读进内存并执行,叫做进程
- 进程是操作系统进行资源分配(包括CPU、内存、磁盘IO等)的最小单位;
- 进程在执行过程中有独立内存单元;
- 进程间通信需要借助操作系统
- 线程是比进程更小的,CPU调度和分配的基本单位
- 保证了多线程的高并发性;
- 进程可以有多个线程,线程之间独立运行,共享进程资源;
- 创建线程时无须寻找内存空间,线程时运行在进程的地址空间中的;
- 不利于资源的管理和保护;
- 操作系统要为每个线程,在进程中的内存空间中分配一个栈,因为每个执行流都需要一个栈来保存各自信息;
- 将程序读进内存并执行,叫做进程
- 线程调度
- 将进程分配给CPU,但是CPU在各个线程之间进行调度;
- 多个CPU也可以并行执行同一进程的多个线程——一个进程中有多个执行流;
- 在进程开启后,创建多个线程即可让所有CPU都慢起来,实现高并发;
- 操作系统的调度器只调度线程,而不调度进程;
- 大量创建线程会消耗过多内存资源,创建销毁需要消耗系统时间;
- 通过线程池技术复用线程,可以避免频繁地创建、销毁线程,而避免内存开销以及时间开销。
- 并发(Concurrency) v.s. 并行(Parallelism)
- 并发:同一时间段,两个任务都同时处于已开始-未结束的状态;
- 并行:多个CPU核心各自同时执行进程,两个线程不相互抢占CPU资源;
- 单CPU可实现并发,多CPU可实现并行;
- 通过CPU时间分片技术将CPU分配给各个线程用于执行,其他线程挂起;并发的线程对应的时间片是乱序的;
- 并发执行多任务,程序之间相互依赖,并且切换程序费时;并行执行多任务,相互独立。
- 线程任务
- 生命周期
- Long-lived Task
- Short-lived Task
- 所需资源
- CPU-Intensive:花费大量时间进行计算,线程数约等于核数即可充分利用CPU资源
- I/O-Intensive:花费大量时间处理磁盘、网络I/O;要考虑IO上的等待时间(Waiting Time),以及CPU上的计算时间(Computing Time)
- 生命周期
- 线程池
- 将需要数据和处理数据的函数交给线程池。此时,线程池中被阻塞在队列上的线程会被被唤醒,执行处理函数;
- 把不同类别的任务放到不同的线程池中;
- 包含I/o任务的线程池需要对任务进行超时设置,否则可能处于阻塞状态。
- Java中的集合并发修改:一个线程遍历集合,另一个并发线程修改了集合
- Fail-Fast
- 并发修改的情况下,抛出异常
ConcurrentModificationException
; - 通过方法 checkForComodification 来检查变量 expectedModCount 和 modCount 是否相等,不等则抛出异常;
- ForEach增强for循环,也会被处理成迭代器(Iterator)来进行迭代;
- 并发修改的情况下,抛出异常
- Fail-Safe
- 当对象发生结构型修改(遍历、添加、删除)时不抛出任何异常,仍然可以正常遍历;
- Fail-Safe类型的数据结构在遍历时会创建迭代器(Iterator);
- 执行的结构型修改不会作用到原集合类对象,因为遍历的是拷贝到迭代器中的副本(需要占用额外的内存空间);
- 可以通过 weakly consistent 来实现;
- java.util.concurrent 下的容器都是安全失败的。
- Fail-Fast
- 操作系统中的并发工具
- 信号量(Semaphore)
- 整型变量来累计唤醒次数,为0的时候表示休眠(挂起);当其他进程挂起,则可以恢复运行。
- 针对其有两个操作:down,up;
- 值大于0,减1;
- 值等于0,挂起;
- 没有进程挂起,加1
- 互斥量(Mutex)
- 一位二进制数(布尔值),0代表解锁,1代表加锁
- 互斥锁处于锁定状态,调用线程会阻塞到线程执行结束,并执行
mutex_unlock
解锁;多个调用线程被阻塞,会随机选一个获得锁。 - 互斥锁处与解锁状态,会调用
mutex_lock
加锁,调用线程成功。
- 快速用户空间互斥(Fast User Space Mutex;Futex)
- 组成部分
- 内核服务:等待队列,允许多个在锁上排队的进程,除非内核解除阻塞
- 用户库
- 组成部分
- 信号量(Semaphore)
- 并发问题的两种锁
- 互斥锁
线程之间共享资源是互斥的,线程获取资源使用权之后就会加锁,使用完之后解锁。这个过程中其他想要获取资源的线程会陷入阻塞睡眠状态,直到资源解锁后(所有被阻塞的线程都)会被唤醒,随机一个线程获得资源使用权后加锁,执行线程,其他的线程继续阻塞。 - 自旋锁
- 特殊的互斥锁。资源被占用后加锁,其他待获取资源的线程不会阻塞,而是陷入循环检测——循环检查资源状态直到资源被释放。
- 好处是减少了线程从阻塞到被唤醒得到资源消耗,缺点是循环检查的过程会一直占用CPU资源。
- 适用于不希望花费太多资源在线程的唤醒上的任务。
- 需要阻止代码运行中的并发干扰
- 中断:硬件中断,软件中断
- 内核抢占
- 互斥锁
- 参考资料