一、概述
TPS:Transactions Per Second
每秒处理事务,是衡量一个服务性能高低的重要指标
二、硬件的效率与一致性
绝大多数运算任务需要处理器和内存进行 IO 交互。
计算机的存储设备与处理器的运算速度有几个数量级的差距,因此高速缓存被用来做为缓冲。
高速缓存:将运算所需要的数据复制到缓存中,让运算能够快速进行,结束后再从缓存同步回主内存之中。
每个处理器都有自己的高速缓存,这样会带来缓存一致性的问题。所以需要遵循一些协议来避免这个问题。
不同的物理机有不同的内存模型,Java 虚拟机也有自己的内存模型。
三、Java 内存模型
Java 虚拟机用 Java 内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现一致的内存访问效果。
1、主内存与工作内存
- 变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为他们是线程私有的
-
所有的变量都存储在主内存中,每条线程有自己的工作内存
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
- 不同线程也无法访问对方工作内存中的变量
2、内存间相互操作
- lock:将主内存的变量标识为一条线程独占的状态
- unlock:将主内存的变量,从锁定状态释放出来
- read:将主内存的变量传输到工作线程的内存中,方便 load 使用
- load:将 read 获取的主内存变量的值放入工作内存的变量副本中
- use:将工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,就会执行这个操作
- assign:将从执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个赋值的指令就执行这个操作
- store:将工作内存中一个变量的值传送到主内存中,方便 write 使用
- write:将 store 操作获取的值放入主内存的变量中
操作规则:
- 不允许 read/load 、store/write 之一单独出现
- 不允许一个线程丢弃她最近的 assign 操作,即变量在工作内存中修改之后必须同步回主内存
- 不允许一个线程无原因的(没有发生 assign 操作)把数据同步回主内存
- 一个新的变量只能在主内存中产生,不允许工作内存中直接使用一个未被 load 或assign 的变量
- 对一个变量 unlock 之前,必须先把此变量同步回主内存
3、volatile 语义
当一个变量用 volatile 修饰之后,具有两种特性:
- 保证此变量对于所有线程的可见性,所有修改会立即更新到主存
- 但是 Java 里的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的
- 禁止指令重排序优化
除了以下两种场景,我们都要加锁(synchronized 或者原子类)来保证原子性:
- 运算结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
4、对于 long 和 double类型变量的特殊规则
允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作,划分为两次 32 位的操作来进行,这就是 long 和 double 的非原子性协定。
不过虚拟机还是选择将这些操作实现为原子性的操作。
5、原子性、可见性和有序性
1)原子性:基本数据类型的访问读写是具备原子性的。Synchronized 块之间的操作也是原子性的
2)可见性:当一个线程修改了变量的值,其他线程可以立即看到。volatile、synchronized、final 都可以保证可见性
3)有序性:如果在本线程内观察,所有的操作都是有序的,如果观察其他线程,所有的操作都是无序的。volatile 和 synchronized 可以保证线程之间的有序性。
6、先行发生原则
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说 A 先行发生于 B,那么就是 B 发生之前,A 的影响能被 B 观察到。
Java 内存模型中天然的先行发生关系,这些关系无需任何同步器协助就已经存在:
- 程序次序规则:在一个线程内,前面代码的操作先于后面的
- 管程锁定规则:一个 unlock 操作先发生于后面对于同一个锁的 lock 操作
- volatile 变量规则:对于一个 volatile 变量的写操作先行发生于后面对于这个变量的读操作
- 线程启动规则:thread 的 start 方法先行发生于此线程的每一个动作
- 线程终止规则:线程的所有操作都先行发生于终止检测
- 线程中断规则:对 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:对象的构造函数先行于 finalize
- 传递性
四、Java 与线程
1、线程的实现
线程是比进程更轻量级的调度单位,各个线程既可以共享进程资源,又可以独立调度。
1)使用内核线程实现
- 内核线程 KTL(Kernel-Level Thread)是直接由操作系统内核支持的线程。
- 由内核来负责完成线程切换。
- 内核通过操纵调度器来对线程进行调度
- 程序一般通过内核线程的高级接口——轻量级进程LWP(Light Weight Process),就是通常意义上的线程 ,和内核是 1:1的关系
缺点:
- 各种线程的创建同步操作,都需要进行系统调用
- 系统调用需要在用户态和内核态中来回切换,所以代价很高
- 每个轻量级进程都需要一个内核支持,所以一个系统支持的轻量级进程是有限的
2)使用用户线程实现
- 户线程是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
- 用户线程的建立同步销毁和调度全部在用户态中完成,不需要内核的帮助。操作快速而且低消耗,可以支持大规模的线程
- 进程与用户线程是1:N
缺点:
- 没有内核的支援,所有线程操作都要自己处理,很复杂
- 处理阻塞等问题会无法解决
3)混合实现
内核线程与用户线程一起使用。
- 用户线程的创建切换依然廉价,并且支持大规模并发
- 内核提供线程调度和处理器映射,大大降低了整个进程被阻塞的风险
- 进程与用户线程是 N:M 的关系
Sun JDK 的 Windows 和 Linux 版本使用 1 对 1 的线程模型
Solaris 平台,可以同时支持 1 对 1 和 多对多的线程模型
2、Java 线程调度
分为协同式线程调度和抢占式线程调度。
协同式线程调度:
- 线程的执行时间由线程本身来控制,当自己的事情做完之后才会通知下一个线程
- 实现简单,但是容易阻塞
抢占式线程调度:
- 每个线程由系统来分配时间,线程的切换不由线程本身来确定
- 不会有一个线程导致整个进程阻塞的问题
3、状态转换