JVM高效并发----Java内存模型与线程
目录
1 概述
- 如何实现多线程
- 多线程之间爱你由于共享和竞争数据导致的一系列问题和解决方案
2 硬件效率与一致性
- 现代计算机系统为了解决存储设备和处理器的运算速度差别较大的问题:
<>加了一层读写速度尽可能接近处理器从运算速度的高速缓存。
<>代码乱序执行优化
- 高速缓存的引入带来的新的问题:缓存一致性
- 多处理器系统中,每个处理器有自己的高速缓存,同时共享一个主内存。
3 Java 内存模型
3.1 主内存和工作内存
- 主要目标是:定义程序中各个变量的访问规则
- Java内存模型中定义了所有变量都存储在主内存中,每条线程有自己的工作内存(和堆、栈、方法区等不适一个层次的内存划分)
3.2 内存间的交互操作
- 关于变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存。
- lock、unlock、read、load、use、assign、store、write
- 必须顺序执行 read 和 load 操作,必须顺序执行store 和 write 操作
3.3 volatile 变量的特殊规则
- 最轻量级的同步机制
- 【特性1】保证此变量对所有线程的可见性
汇编代码中的lock 前缀操作。他的作用是死的本CPU的Cache写入了内存,也会引起别的CPU或者内核无效化其Cache,相当于是对volatile 修饰的变量进行了一次 store 和write 操作。所以 volatile 变量的修改对其他CPU立即可见。
- 【特性2】禁止指令重排序优化
汇编代码中的lock 前缀操作。相当于一个内存屏障(重排时是不能把后面的指令排序到内存屏障之前的位置)。所以lock 前缀操作指令把修改同步到内存时,以为这之前的所有操作已经执行完毕。形成了“指令重排无法越过内存屏障”的效果。
- volatile 变量只能保证可见性,不符合以下情况的话,仍然要通过加锁保证原子性
<1>运算结果不依赖变量的当前值,或者确保只有单一线程修改变量的值
<2>变量不需要与其他状态的变量共同参与不变约束
- Java内存模型对于 volatile 变量定义的特殊规则:
<1>使用变量前必须从主内存刷新最新的值
<2>每次修改变量之后必须立即同步回主内存
<3>volatile 修饰的变量不会被指令重排优化
3.4 原子性、可见性、有序性
- 原子性
- 基本数据类型的访问读写具有具备原子性
- 字节码指令 monitorenter 和monitorexit 隐式的使用率lock 和unlock 操作。(这两个字节码指令在Java代码中就是 synchronized 关键字)
- 可见性
- Java内存模型是通过在变量修改后将性质同步回内存,在变量读取前从主内存刷新变脸之这种依赖主内存作为媒介的方式来实现可见性的。
- volatile 变量和普通变量的区别:volatile 的特殊规则保证了新值能够立即同步到主内存,以及每次使用前立即从主内存刷新。普通变量不能保证。
- synchronized 关键字实现可见性。由“对变量执行unlock 之前必须把此百年来同步回主内存中”实现。
- final 关键字实现可见性。被final 修饰的字段在构造器中一旦完成初始化,并且不发生this 引用逃逸,其他线程中就能看见final 字段的值。
- 有序性
- 总结:在本线程中观察,所有操作都是有序的;在一个线程中观察另一个线程,所有操作都是无序的。
- volatile 关键字,本身禁止指令重排。
- synchronized 由“一个变量在同一时间只允许一条线程对其进行lock操作”获得的。这说明持有同一个锁的两个同步块只能串行的进入。
3.5 先行发生原则
- 是判断是否存在竞争、线程是否安全的主要依据。
- 天然的先行发生关系:
<1> 程序次序规则:线程中的控制流顺序
<2>管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
<3> volatile 变量规则:一个 volatile 变量的写操作先行发生于后面对这个变量的读操作
<4> 线程启动规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
<5> 线程终止规则:Thread 对象的的所有操作都先行发生于此线程的终止检测。
<6> 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
<7> 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
<8> 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
- 时间先后顺序与先行发生原则直接按基本没有太大关系。
4 Java 与线程
4.1 线程的实现
- Java 中Thread 类所有关键方法都声明是Native 。以为这这个方法没有使用或者无法使用平台无关技术。
- 实现线程的3中方式:
<1> 使用内核线程实现
>程序使用内核线程的一种高级接口--轻量级线程(LWP,也就是我们通常所讲的线程)
>轻量级线程与内核线程之间是1:1的。
>局限性:
a)各种现场的操作都需要系统调用,系统调用的代价较高,会有用户态和内核态的切换;b)消耗一定的内核资源,数量有限。
<2> 使用用户线程实现
>用户线程的建立、同步、销毁和调度完全在用户态完成.;可以支持规模更大的线程数量。
>进程与用户线程之间1:N
<3> 使用用户线程加轻量级进程混合实现
>操作系统提供支持的轻量级进程作为用户线程和内核线程之间的桥梁;可以使用内核提供的线程调度功能及处理器映射;用户线程的系统调用通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。
>用户线程和轻量级进程N:M;
4.2 Java线程的实现
- 操作系统支持正义的现场模型,很大程度上决定了Java虚拟机的现场模型是怎样映射的。
4.3 Java线程调度
- 线程调度:为线程分配处理器使用权的过程
- 分为两种:
<1> 协同式调度
好处:简单、切换操作对线程可知,不存在线程同步问题。
坏处:线程执行时间不可控。
<2>抢占式调度
线程的执行时间是系统可控的。-------Java采用的线程调度方式。
Java 设置有现成优先级。
线程优先级不太靠谱:1)在一些平台上不同的优先级可能变的相同;2)优先级可能会被系统自行改变(window中的优先级推进器等。)
4.4 状态转换
Java有5种线程状态:
- 新建
创建,未执行
- 运行
可能在运行,可能在等待CPU 分配执行时间
包括操作系统线程状态中的Running 和 Ready。
- 无限期等待
不会被分配CPU时间。需要其他线程显示唤醒。
以下方法会让线程无限期等待:
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
- 限期等待
不会被分配CPU时间。;不需要其他线程显示唤醒。
以下方法会让线程限期等待:
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
- 阻塞
阻塞和等待的区别是:阻塞状态在等待获取到一个排他锁。这个事件将在一个线程放弃这个锁的时候发生;等待状态这是在等待一段时间或者唤醒动作的发生。
- 结束
终止线程状态。
参考:
《深入理解Java虚拟机》