Java内存模型
Java内存模型定义了一种多线程访问Java内存的规范。
- Java内存模型将内存分为了主内存和工作内存。类的状态也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
- 定义了几个原子操作,用于操作主内存和工作内存中的变量
- 定义了volatile变量的使用规则
- happens-before即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
硬件模型
线程读取主内存的数据到CPU缓冲中,当数据放在不同位置时,会有两个问题:可见性与静态条件
多个线程之间是可以使用PipedInputStream/PipedOutputSteam互相传递数据通信的,它们之间的沟通只能通过共享变量来进行
- Java内存模型JMM规定了JVM有主内存,主内存是多个线程共享的
- 当new一个对象时,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的
public class T1 {
private int num;
public static void main(String[] args) {
T1 t = new T1();
t.pp();
}
public void pp() {
//通过对num的操作实现了t1和t2之间的通信,这里目前不保证输出的正确性.可以通过同步锁synchronized保证数据的正确性
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++)
add();
});
new Thread(() -> {
for (int i = 0; i < 100; i++)
sub();
}).start();
t1.start();
}
public void add() {
num++;
System.out.println(Thread.currentThread() + "加法:" + num);
}
public void sub() {
num--;
System.out.println(Thread.currentThread() + "减法:" + num);
}
}
Java中堆和栈有什么不同
每个线程都有自己的栈内存(栈帧),用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。
而堆是所有线程共享的一片公用内存区域
JDK1.6+引入了逃逸分析,对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值
如何在Java中获取线程堆栈
对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。也可以用jstack这个工具来获取,它对线程id进行操作,可以用jps这个工具找到id。
通过使用 jps 检查当前正在运行的JAVA进程的 PID。jps –lvm
使用明确的 PID 作为 jstack 的参数来获取 thread dumps。jstack -f 5824
一般用于死锁的分析和线程执行速度很慢时的分析
JVM中哪个参数是用来控制线程的栈堆栈小的
-Xss参数用来控制线程的堆栈大小
Xms2048m:设置JVM初始内存为2048m,若设置与-Xmx不同,每次垃圾回收完成后JVM重新分配内存
-Xmx4096m:设置JVM最大可用内存为4096M
-Xss1024k:设置每个线程的堆栈大小为1024K。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K,可以根据应用的线程所需内存大小进行调整。在相同物理内存下减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
-XX:PermSize=512m:设置非堆区初始内存分配大小为512m,其缩写为permanent size(持久化内存)
-XX:MaxPermSize=512m:设置对非堆区分配的内存的最大上限为512m
注:在配置之前一定要慎重的考虑一下自身软件所需要的非堆区内存大小,因为此处内存是不会被java垃圾回收机制进行处理的地方。并且更加要注意的是最大堆内存与最大非堆内存的和绝对不能够超出操作系统的可用内存。
线程操作某个对象的执行顺序
从主存赋值变量到当前工作内存read and load
执行代码,改变共享变量值use and assign
用工作内存数据刷新主存相关内容store and write
volatile关键字
volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行原子性。而最彻底的同步要保证有序性、可见性和原子性的synchronized
任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是原子的
public class VolatileTest {
public volatile int a;
public void add(int count) {
a = a + count;
}
}
volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁
要使 volatile 变量提供理想的线程安全,必须同时满足两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
- 保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其他线程立即可见
- 保证有序性:当变量被修饰为 volatile 时,JMM
会禁止读写该变量前后语句的大部分重排序优化,以保证变量赋值操作的顺序与程序中的执行顺序一致 - 部分原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性
volatile的认识
public class Test1 {
private static boolean flag = false;
private static int i = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
flag = true;
System.out.println("flag changed...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (!flag) {
i++;
}
System.out.println("progress end...");
}
}
程序不能执行结束,会进入死循环状态。
解决方案:ag上添加关键字volatile
总结
Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。
1、线程内的代码能够按先后顺序执行,这被称为程序次序规则。
2、对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
3、前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
4、一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
5、一个线程的所有操作都会在线程终止之前,线程终止规则。
6、一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
7、可传递性。如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论