程序的执行
一个程序的执行,首先把可执行文件读入内存转换为二进制文件(全是0和1构成)
找到起始(main)方法的地址逐步读出指令与数据
进行计算并写回到内存中;
从内存读入到CPU计算要通过总线;
总线分为三种:控制线 地址线 数据线 可以区分对应的二进制文件是数据、指令还是表示地址
进程 线程
一个程序被读入内存,称之为进程;同一个进程内部;有多个任务并发执行的需求(比如:一边计算,一边接受网络数据,一边刷新界面)
能不能用多进程?
可以,但是问题多,最严重的问题是我们可以很轻易的搞死别人的进程
多线程的概念出现:共享空间,不共享计算
进程是静态的概念,程序进入内存,分配对应的资源(内存空间);进程进入内存同时产生一个主线程
线程是动态的概念,是可执行的计算单元(任务)
一个ALU同一个时间只能执行一个线程
线程:操作系统进行运行调度的最小单位。
进程:系统进行资源分配和调度的基本单位。
区别与联系:一个进程可以有一个或多个线程;线程包含在进程之中,是进程中实际运行工作的单位;进程的线程共享进程资源;一个进程可以并发多个线程,每个线程执行不同的任务。
线程切换
切换时:保存上下文,保存现场
问题:是不是线程数量越多,执行效率越高?
不是
问题:单核CPU多线程是否有意义?
有,当一个线程需要等待数据读入时,另外的线程可以进行计算
问题:对于一个程序,设置多少个线程合适;(线程池设定多少核心线程)
CPU的并发控制
关中断
缓存一致性协议
CPU的速度和内存的速度(100:1):这里的速度指的是ALU访问寄存器的速度比访问内存的快100倍
为了充分利用CPU的计算能力,在CPU和内存中引入缓存的概念(现在工业实践,多采用三级缓存的架构)
缓存行:指一次性读取数据块
程序的局部性原理:空间局部性 时间局部性
如果缓存行大,命中率高,但是读取效率低;
如果缓存行小,命中率低,但是读取效率高。
工业实践的妥协结果:目前(2021)的计算机多采用64bytes(64*8bit 位)为一行
由于缓存行的存在,我们必须要有一种机制,来包装缓存数据的一致性,这种机制称为缓存一致性协议
缓存行代码示例
public class CacheLinePadding3 {
public static long count = 10000_0000L;
// 1 byte = 8 bit
// long占 8byte(64bit位)
// 加上x刚好 8*8byte = 64bytes
private static class Padding{
public volatile long p1 ,p2 ,p3 ,p4 ,p5 ,p6 ,p7;
}
//private static class T {
private static class T extends Padding {
public volatile long x = 0l;
}
public static T[] arr =new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread (()->{
for (long i = 0;i<count;i++){
arr[0].x = i;
}
});
Thread t2 = new Thread (()->{
for (long i = 0;i<count;i++){
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/count);
}
}
第13行使用继承耗时
第12行不使用继承耗时
对象创建的过程
不要在构造方法中启动线程
创建一个类
package com.gzcctd.msb;
public class A {
}
创建对象
package com.gzcctd.msb;
public class Test {
public static void main(String[] args) {
A a = new A();
}
}
查看对应的汇编语言
// class version 52.0 (52)
// access flags 0x21
public class com/gzcctd/msb/Test {
// compiled from: Test.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/gzcctd/msb/Test; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
NEW com/gzcctd/msb/A
DUP
INVOKESPECIAL com/gzcctd/msb/A.<init> ()V
ASTORE 1
L1
LINENUMBER 6 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE a Lcom/gzcctd/msb/A; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
}
NEW com/gzcctd/msb/A #创建A对象
DUP
INVOKESPECIAL com/gzcctd/msb/A.<init> ()V #初始化对象
ASTORE 1 #引用指向
RETURN
#4、5行指令可能重排序
单例模式
饿汉式
/**
* 饿汉式单列模式
* 类加载到内存后,就实例化一个单列,JVM保证线程安全
* 简单实用,推荐实用!
* 唯一缺点:补光用到与否,类加载试就完成实例化
*/
public class HungrySingle {
private HungrySingle(){
System.out.println(Thread.currentThread().getName()+"->创建");
}
private final static HungrySingle HUNGRY_SINGLE = new HungrySingle();
public static HungrySingle getHungrySingle(){
return HUNGRY_SINGLE;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
HungrySingle instance = HungrySingle.getHungrySingle();
},i+"").start();
}
}
}
DCL单列
//双重检测锁 模式懒汉单例~线程安全 DCL 避免指令重排
public class LazyMan02 {
private LazyMan02(){
System.out.println(Thread.currentThread().getName() + "->创建");
}
//所以要避免指令重拍 添加volatile
private volatile static LazyMan02 lazyMan;
//不加双重校验在多线程下不安全
public static LazyMan02 getInstance(){
if(lazyMan==null){
synchronized (LazyMan02.class){
if(lazyMan==null){
lazyMan = new LazyMan02();//这行代码不是一个原子性操作
/**
* 三步:1分配内存空间;2执行构造方法,初始化对象;3把初始化对象指向分配的空间
* 期望指令为123
* 但是可能为132 线程A执行132
* 线程B //此时线程B来进行校验发现 lazyMan!=null 就返回一个空对象
*
* 所以要避免指令重拍 添加volatile
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan02 instance = LazyMan02.getInstance();
}).start();
}
}
}
哪些指令可以互换顺序
Happens-before原则(JVM规定重排序必须遵守的规则)
程序次序规则:同一个线程内,安装代码出现的顺序,前面的代码先与后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构
管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的Lock操作
Volatile变量规则:对于一个volatile变量的写操作先行发生后面(时间上)对同一个锁的Lock操作
线程启动规则:Thread的start()方法先行发生于这个线程的每一个操作
线程终止规则:线程的所有操作都先行此线程的终止检测;可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测线程的终止
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测线程是否中断
对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始
传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
CPU乱序执行
public class JMM {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;){
i++;
x=0;
y=0;
a=0;
b=0;
CountDownLatch down = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
down.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
down.countDown();
}
});
t1.start();
t2.start();
//t2.join();
//t2.join();
down.await();
String result = "第"+i+"次("+x+","+y+")";
if(x==0 && y==0){
System.err.println(result);
break;
}
}
}
}
打印输出在第1117837次时出现了乱序
JMM程序的排列组合
通过上面的排列组合可以看出JMM这个程序,证明乱序执行的确实存在
为什么会乱序?
主要是为了提高效率(在等待费时的指令执行的时候,优先执行后面的指令)
线程的as-if-serial
单个线程,两条语句未必是按顺序执行
单线程的重排序,必须保证最终一致性
as-if-serial:看上去像是序列化(单线程)
不管如何重排序,单线程执行结果不会改变
禁止编译器乱排序
使用内存屏障阻止指令乱序执行
内存屏障是特殊指令:看到这种指令,前面的必须执行完成,后面才执行
intel:lfence sfence mfence(CPU特有指令)
X86CPU内存屏障
- sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成
- lfence:在lfence指令钱的都操作当必须在lfence指令后的读操作前完成
- mfence:在mfence指令钱的读写操作当必须在mfence指令后的读写操作前完成
intel lock汇编指令
原子指令,如x86上的lock指令是一个fullbarrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU.software locks 通常使用了内存屏障或者原子指令来实现变量可见性和保持程序顺序
JVM中的内存屏障
所有实现JVM规范的虚拟机,必须实现四个屏障
- LoadLoad屏障:
对于这样的语句Load1;LoadLoad;Load2
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 - StoreStroe屏障:
对于这样的语句Store1;StoreStore;Store2
在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。 - LoadStroe屏障:
对于这样的语句Load1;LoadStore;Store2
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 - StroeLoad屏障:
对于这样的语句Store1;StoreLoad;Load2
在Load2及后续所有读取操作执行前,保证Store1的写入操作对其他处理器可见。
volatile的底层实现
- 可见性
- 禁止指令重拍
volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
基于jvm的内存屏障
# 写
---StoreStroeBarrier---
# volatile 写代码
---StoreLoadBarrier---
#读
# volatile 读代码
---LoadLoadBarrier---
---LoadStoreBarrier---
hotspot实现
总结:
- LOCK用于在多处理器中执行指令时对共享内存的独占使用
- 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效
- 另外还提供了有序的指令无法越过这个内存屏障的作用