概述
JVM
Java虚拟机,运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(称作栈空间)
JMM
Java内存模型,是一种抽象的概念,描述的是一组规范,通过这组规范定义了;程序中各个变量(包括实例字段,静态字段,构成数组对象的元素)的访问方式
JMM关于同步的规定
1、线程加锁前,必须读取主内存的最新值到自己的工作内存
2、线程解锁前,必须把共享变量的值刷新回主内存
3、加锁解锁是同一把锁
JMM三特性
可见性:主物理内存的值只要被修改,其他线程马上会获取通知的机制
原子性:某个线程正在做某个具体业务时,不允许加塞或者被分割,需要整 体完成,要么同时成功,要么同时失败
有序性:经过指令重排,字节码指令的顺序可能与我们源代码顺序不一致,处理器在指令重排时必须要考虑数据之间的依赖性
1 > 单线程中最终指令运行结果和程序顺序执行结果一致,
2 > 在多线程中结果无法预测,所以必须保证线程执行的有序性
可见性、原子性、有序性共同保证了线程的安全性
线程对变量的操作三部曲
1 > 将变量拷贝到自己的工作内存
2 > 对变量进行修改
3 > 修改完成后再将变量写回主内存
原因 :
工作内存是每个线程的私有数据区域,JMM规定所有变量都存储在主内存,主内存是共享区域,所有线程都可以访问,但线程对变量的操作必须在自己的工作内存中进行
注:
i++不是原子性操作
不能直接操作主内存,所以线程要互相进行通信,必须通过主内存进行
缺少可见性控制(写不可见)
public class Main implements Runnable{
boolean f;
@Override
public void run() {
try {
Thread.sleep(1000);
f=true;
} catch (Exception e) {
}
}
public static void main(String[] args) {
Main m=new Main();
Thread t=new Thread(m,"A");
t.start();
while(!m.f) {}
System.out.println("f为true,线程A将f修改成功");
}
}
线程调入死循环,一直无输出
分析:f的可见性未得到保证,线程A虽修改成功,但主线程并不知道
缺少原子性控制(写丢失)
public class Main implements Runnable{
volatile int sum;
@Override
public void run() {
for(int j=0;j<200;j++) {
sum++;
}
}
public static void main(String[] args) {
Main m=new Main();
for(int i=0;i<20;i++) {
Thread t=new Thread(m);
t.start();
}
while(Thread.activeCount()>2) {}
System.out.println(m.sum);
}
}
结果分析:sum<=4000
原因:sum++不是一个原子性操作,并且Volatile无法保证原子性
缺少有序性控制(指令重排)
public void M() {
int x=1;//语句1
int y=2;//语句2
x=x+5;//语句3
y=x*x;//语句4
}
执行结果顺序可能是1234、1324、2134
语句4可能为第一条执行吗? 不可能,因为y依赖于x,存在数据间的依赖性
在单线程下不影响结果正确性
int x=1;
boolean f;
public void m1() {
x=1;
f=true;
}
public void m2() {
while(!f) {
x+=5;
System.out.println(x);
}
}
//线程1执行m1,线程2执行m2
输出结果为6或5(线程1执行m1发生了指令重排)
多线程下导致输出结果不一致
总结:
- 更改不可见(可见性):
主物理内存的值只要被修改,其他线程马上会获取通知的机制 - 写丢失(原子性):
多个线程同时拿到同一资源,线程1将资源做了修改,此时线程2在获取 资源更改的通知前(可见性),也修改了资源,导致线程1修改丢失,出现了写覆盖情况 - 执行乱序(有序性):
指令重排在编译器认为一种优化,但却使我们的程序在多线程中的数据一致性无法保证,所以
执行乱序解决方案:
synchronized:每次只有一个线程进入关键区,单线程指令重排不存在问题
volatile:禁止指令重排