在多线程编程中,就涉及到线程之间的通信。为了更好的实现程序的高并发、高性能、高可用,就不得不知道JMM。至于高可用,今后再详细总结。另外,此处有一文介绍HA,参考浅述实现系统高可用,常用的解决手段 | 《Linux就该这么学》
一、JMM
1、JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是java整个计算虚拟模型。
2、从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
3、本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
二、JVM对Java内存模型的实现
根据JMM模型,在JVM把内存分成了两部分:线程栈区和堆区。
JVM中运行的每个线程都拥有自己的线程栈(也称调用栈),线程栈包含了当前线程执行的方法调用相关信息。随着代码的不断执行,调用栈会不断变化。
1、线程栈区(Thread stack)
1)线程栈(
thread stack,
也叫线程堆栈),所拥有的资源是独自的,对其他线程不可见。2)线程栈,也包含正在的所有局部变量。
3)由当前线程创建的局部变量,对于非创建它的其他所有线程都是不可见的。
4)即使两个线程正在执行完全相同的代码,两个线程仍然会在每个线程堆栈中创建该代码的局部变量。即便可传递变量副本,但不共享原始局部变量本身。
2、堆区(Heap)
1)堆包含创建的java应用程序对象。
2)堆中的对象可以被具有对象引用的所有线程访问。当一个线程访问一个对象时,它也可以访问该对象的成员变量。
3)如果两个线程同时调用同一个对象上的一个方法,它们都可以访问该对象的成员变量,但每个线程都有自己的局部变量副本
4)堆中的数据是共享的,线程不安全的
详细说明:
- 所有原始类型(boolean,byte,short,char,int,long,float,double)的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的局部变量,一个线程可以传递一个变量副本给另一个线程,但原始变量是不共享的。
- 堆区包含了Java应用创建的所有对象信息(包括原始类型的封装类),不管对象是哪个线程创建的,不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区。
- 一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
- 对于一个对象的成员方法,这些方法中包含局部变量,仍需要存储在栈区,即使它们所属的对象在堆区。
- 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。
- Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
基于JMM的JVM模型,既然堆中的数据是共享的,那么在多线程环境中,就可能存在数据安全性问题。主要涉及到:可见性问题,竞争性问题等。
在此之前,回顾几个点
A、计算机常识:
- cpu执行的操作是原子性的,是不可拆分的。
B、造成数据安全性问题的必要条件:
- 多线程环境
- 多个线程操作共享数据
- 操作共享数据的语句不是原子性的(多条)
此中,引申的。后续补充
3、共享对象的可见性(事例说明)
如果两个或多个线程共享一个对象,但没有正确使用volatile
声明或Synchronized
同步机制,一个线程更新了共享变量值后,对于其他线程来讲是不可见的。如线程A,线程B同时要进行modify。
public class Account { private float balance; public void modify (float difference) { float value=this.balance; this.balance=value+difference; } }
首先,线程A和线程B在各自的thread stack
中维护了一分局部变量的副本,线程A修改了线程A中Thread stack中的局部变量,但是还没有还没将修改的数据刷新到Main Memory
中,而线程B获取的值依然是old value
,就会出现问题
解决方案
- 使用
volatile
关键字 - 使用
synchronized
同步机制
tips:volatile与synchronized的区别:
- volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
- volatile仅修饰变量;synchronized则可以修饰变量、方法、代码块
- volatile仅保证可见性;synchronized则可以保证可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
- volatile修饰的变量会禁止指令重排序,因而程序不会被编译器优化;synchronized修饰的变量没有禁止指令重排序,因而程序可以被编译器优化
三、JVM(JDK1.8中取消了永久代。改为元空间,直接使用内存区域)
- 程序计数器(PC)
- java虚拟机栈
- 本地方法栈
- java堆
- 方法区
1、程序计数器(PC)
是一块很小的内存空间,用于记录将要运行的指令。
tips:每个线程都需要一个程序计数器;各个线程的计数器相互独立,是私有的。
2、java虚拟机栈
保存了局部变量、部分结果,并参与方法的调用和返回,的一个内存空间
tips:1)对各个线程来说,也是私有的;
2)它和java线程同一时间创建;
3)由java语言实现的
3、本地方法栈
与java虚拟机栈的功能相似
tips:1)java虚拟机栈用于管理Java函数的调用,本地方法栈用于管理本地方法的调用;
2)由C语言实现的
4、java堆
tips:1)做存储的,为所有创建的对象和数组分配内存空间;
2)被JVM中所有的线程共享
5、方法区
也被称为永久区,与堆空间相似,被JVM中所有的线程共享。
1)方法区主要保存的信息是类的元数据,方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。其中运行时常量池就在方法区;
2)GC对永久区的回收:一是对永久区常量池的回收;二是永久区对元数据的回收
参考鸣谢: