这篇博文之所以和“JVM系列之内存与垃圾回收篇”同期出,是因为我觉得如果单学习JVM的内存部分可能会让大部分人有点不知所云,毕竟不是“JVM系列之性能监控与调优篇”,所以我结合了线程安全和JVM数据运行区域下的变量去说明JMM内存模型的核心作用,结合“JVM系列之内存与垃圾回收篇”,可以让JVM更容易理解。
“JVM系列之JMM内存模型”使用代码和实测的手法来说明知识点,内容是从用户请求到JVM去解析的,涉及变量,WEB服务器,多线程,JVM数据运行区域等知识点,如果有一些知识点有异议,欢迎指出!
也许你觉得这个问题放到这里有点不合适,但是上面提到JMM主要是与变量(数据)有关,所以知道什么环境会导致数据安全,而JMM是怎么控制这个问题的,是了解JMM的核心。
package com.lzx.JVMObject;
/**
* @author lzx
* @version 1.0
*/
public class VariableAndObject {
/**
* 基本实例变量,存在堆空间(值),属于通过VariableAndObject创建的实例对象,不一定线程不安全
*/
int baseInstanceVariable = 0;
/**
* 基本类变量,存在方法区,属于VariableAndObject类,一定线程不安全
*/
static int baseClassVariable = 0;
/**
* 引用实例变量,存在堆空间(引用和值),属于通过VariableAndObject创建的实例对象,不一定线程不安全
*/
Object referenceInstanceVariable = new Object();
/**
* 引用类变量,存在方法区(引用和值),属于VariableAndObject类,一定线程不安全
*/
static Object referenceClassVariable = new Object();
public void method() {
/**
* 基本局部变量,存在栈空间(值),属于method,一定线程安全
*/
int baseLocalVariable = 0;
/**
* 引用局部变量,引用存在栈空间,实例对象存在堆(逃逸的情况下),属于method,一定线程安全
*/
Object referenceLocalVariable = new Object();
}
}
class Object {
/**
* 基本实例变量,存在堆空间(值),属于通过Object创建的实例对象,不一定线程不安全
*/
int baseInstanceVariable = 0;
/**
* 基本类变量,存在方法区,属于Object类,一定线程不安全
*/
static int baseClassVariable = 0;
/**
* 引用实例变量,存在堆空间(引用和值),属于通过Object创建的实例对象,不一定线程不安全
*/
Object referenceInstanceVariable = new Object();
/**
* 引用类变量,存在方法区(引用和值),属于Object类,一定线程不安全
*/
static Object referenceClassVariable = new Object();
public void method() {
/**
* 基本局部变量,存在栈空间(值),属于method,一定线程安全
*/
int baseLocalVariable = 0;
/**
* 引用局部变量,引用存在栈空间,实例对象存在堆(逃逸的情况下),属于method,一定线程安全
*/
Object referenceLocalVariable = new Object();
}
}
package com.lzx.JVMObjectThreadSafe;
import java.util.ArrayList;
import java.util.List;
/**
* 局部变量线程安全测试,结果:局部变量一定线程安全
*
* @author lzx
* @version 1.0
*/
public class LocalVariableThreadSafe {
public static void main(String[] args) {
LocalVariableThreadSafeByRunnable localVariableThreadSafeByRunnable = new LocalVariableThreadSafeByRunnable();
for (int i = 0; i < 2; i++) {
LocalVariableThreadSafeByThread localVariableThreadSafeByThread = new LocalVariableThreadSafeByThread();
localVariableThreadSafeByThread.setName("资源不共享的线程" + i + "创建方式");
localVariableThreadSafeByThread.start();
Thread thread = new Thread(localVariableThreadSafeByRunnable);
thread.setName("资源共享的线程" + i + "创建方式");
thread.start();
}
}
}
/**
* 资源不共享的线程创建方式
*/
class LocalVariableThreadSafeByThread extends Thread {
@Override
public void run() {
baseLocalVariableThreadSafeTest();
referenceLocalVariableThreadSafeTest();
}
/**
* 基本局部变量线程安全测试
*/
public void baseLocalVariableThreadSafeTest() {
int num = 0;
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用局部变量:" + num);
}
/**
* 引用局部变量线程安全测试
*/
public void referenceLocalVariableThreadSafeTest() {
List list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用局部变量:" + list.toString());
}
}
/**
* 资源共享的线程创建方式
*/
class LocalVariableThreadSafeByRunnable implements Runnable {
@Override
public void run() {
baseLocalVariableThreadSafeTest();
referenceLocalVariableThreadSafeTest();
}
/**
* 基本局部变量线程安全测试
*/
public void baseLocalVariableThreadSafeTest() {
int num = 0;
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的基本局部变量:" + num);
}
/**
* 引用局部变量线程安全测试
*/
public void referenceLocalVariableThreadSafeTest() {
List list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的引用局部变量:" + list.toString());
}
package com.lzx.JVMObjectThreadSafe;
import java.util.ArrayList;
import java.util.List;
/**
* 实例变量线程安全测试,结果:该实例对象在共享的线程内(使用implements Runnable去创建的线程),那该实例对象是线程不安全的,
* 如果该实例对象在不共享的线程内(使用extends Thread去创建的线程),那该实例对象是线程安全的
*
* @author lzx
* @version 1.0
* @date 2020/9/14 15:40
*/
public class InstanceVariableThreadSafe {
public static void main(String[] args) {
InstanceVariableThreadSafeByRunnable instanceVariableThreadSafeByRunnable = new InstanceVariableThreadSafeByRunnable();
for (int i = 0; i < 100; i++) {
InstanceVariableThreadSafeByThread instanceVariableThreadSafeByThread = new InstanceVariableThreadSafeByThread();
instanceVariableThreadSafeByThread.setName("资源不共享的线程" + i + "创建方式");
instanceVariableThreadSafeByThread.start();
Thread thread = new Thread(instanceVariableThreadSafeByRunnable);
thread.setName("资源共享的线程" + i + "创建方式");
thread.start();
}
}
}
/**
* 资源不共享的线程创建方式
*/
class InstanceVariableThreadSafeByThread extends Thread {
int num = 0;
List list = new ArrayList<>();
@Override
public void run() {
baseInstanceVariableThreadSafeTest();
referenceInstanceVariableThreadSafeTest();
}
/**
* 基本实例变量线程安全测试
*/
public void baseInstanceVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用实例变量:" + num);
}
/**
* 引用实例变量线程安全测试
*/
public void referenceInstanceVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用实例变量:" + list.toString());
}
}
/**
* 资源共享的线程创建方式
*/
class InstanceVariableThreadSafeByRunnable implements Runnable {
int num = 0;
List list = new ArrayList<>();
@Override
public void run() {
baseInstanceVariableThreadSafeTest();
referenceInstanceVariableThreadSafeTest();
}
/**
* 基本实例变量线程安全测试
*/
public void baseInstanceVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的基本实例变量:" + num);
}
/**
* 引用实例变量线程安全测试
*/
public void referenceInstanceVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的引用实例变量:" + list.toString());
}
}
package com.lzx.JVMObjectThreadSafe;
import java.util.ArrayList;
import java.util.List;
/**
* 类变量线程安全测试,结果:类变量是属于类的,在类创建的时候就已经存在,多个线程共享,所以类变量是线程不安全的
*
* @author lzx
* @version 1.0
* @date 2020/9/14 15:40
*/
public class ClassVariableThreadSafe {
public static void main(String[] args) {
ClassVariableThreadSafeByRunnable classVariableThreadSafeByRunnable = new ClassVariableThreadSafeByRunnable();
for (int i = 0; i < 100; i++) {
ClassVariableThreadSafeByThread classVariableThreadSafeByThread = new ClassVariableThreadSafeByThread();
classVariableThreadSafeByThread.setName("资源不共享的线程" + i + "创建方式");
classVariableThreadSafeByThread.start();
Thread thread = new Thread(classVariableThreadSafeByRunnable);
thread.setName("资源共享的线程" + i + "创建方式");
thread.start();
}
}
}
/**
* 资源不共享的线程创建方式
*/
class ClassVariableThreadSafeByThread extends Thread {
static int num = 0;
static List list = new ArrayList<>();
@Override
public void run() {
baseClassVariableThreadSafeTest();
referenceClassVariableThreadSafeTest();
}
/**
* 基本类变量线程安全测试
*/
public void baseClassVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用类变量:" + num);
}
/**
* 引用类变量线程安全测试
*/
public void referenceClassVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(extends Thread)的引用类变量:" + list.toString());
}
}
/**
* 资源共享的线程创建方式
*/
class ClassVariableThreadSafeByRunnable implements Runnable {
static int num = 0;
static List list = new ArrayList<>();
@Override
public void run() {
baseClassVariableThreadSafeTest();
referenceClassVariableThreadSafeTest();
}
/**
* 基本类变量线程安全测试
*/
public void baseClassVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的基本类变量:" + num);
}
/**
* 引用类变量线程安全测试
*/
public void referenceClassVariableThreadSafeTest() {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的引用类变量:" + list.toString());
}
}
package com.lzx;
public class JVMObjectTest {
private final static StringBuilder finalStaticSB = new StringBuilder("a");
private final StringBuilder finalSB = new StringBuilder("a");
public static void main(String[] args) {
// finalStaticSB = new StringBuilder() //报错,无法指向新引用
finalStaticSB.append("b"); //正常,可以改变值
JVMObjectTest jvmObjectTest = new JVMObjectTest();
//jvmObjectTest.finalSB = new StringBuilder() //报错,无法指向新引用
jvmObjectTest.finalSB.append("b"); //正常,可以改变值
System.out.println(finalStaticSB);
System.out.println(jvmObjectTest.finalSB);
}
}
class MixedOrder{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
解析:存在两条线程A和B,线程A调用实例对象的writer()方法,而线程B调用实例对象的read()方法,线程A先启动而线程B后启动,那么线程B读取到的i值是多少呢?现在依据8条原则,由于存在两条线程同时调用,因此程序次序原则不合适。writer()方法和read()方法都没有使用同步手段,锁规则也不合适。没有使用volatile关键字,volatile变量原则不适应。线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性和本次测试案例也不合适。线程A和线程B的启动时间虽然有先后,但线程B执行结果却是不确定,也是说上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的。修复这个问题的方式很简单,要么给writer()方法和read()方法添加同步手段,如synchronized或者给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见。
关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总是立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作在多线程环境并不保证安全性(不能保证原子性),原因是可能会有两个线程同时读到同一个数值,如下
package com.lzx.KeywordThreadSafe.Volatile;
/**
* Volatile不安全测试,结果:Volatile并不能保证原子性
* @author lzx
* @version 1.0
*/
public class VolatileVisibilityThreadUnSafe {
public static void main(String[] args) {
VolatileVisibilityByRunnable volatileVisibilityByRunnable = new VolatileVisibilityByRunnable();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(volatileVisibilityByRunnable);
thread.setName("资源共享的线程" + i + "创建方式");
thread.start();
}
}
}
/**
* 资源共享的线程创建方式
*/
class VolatileVisibilityByRunnable implements Runnable {
volatile static int num = 0;
@Override
public void run() {
volatileVisibilityThreadUnSafeTest();
}
/**
* volatile原子性线程不安全测试
*/
public void volatileVisibilityThreadUnSafeTest() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的volatile变量:" + num);
}
}
解析: 虽然num变量的任何改变都会立马反应到其他线程中,如果有多条线程同时读到一个值,并对同一个值进行操作,就会出现线程安全问题,毕竟num += i;操作并不具备原子性,该操作是先读取值,然后写回一个新值,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值操作,这也就造成了线程安全失败,因此对于volatileVisibilityThreadSafeTest方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。
另外一种场景,可以使用volatile修饰变量达到线程安全的目的,如下
package com.lzx.KeywordThreadSafe.Volatile;
/**
* Volatile安全测试,结果:Volatile可以保证可见性
* @author lzx
* @version 1.0
*/
public class VolatileVisibilityThreadSafe {
public static void main(String[] args) {
VolatileVisibilityByRunnable2 volatileVisibilityByRunnable2 = new VolatileVisibilityByRunnable2();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(volatileVisibilityByRunnable2);
thread.setName("资源共享的线程" + i + "创建方式");
thread.start();
}
}
}
/**
* 资源共享的线程创建方式
*/
class VolatileVisibilityByRunnable2 implements Runnable {
volatile static boolean close;
@Override
public void run() {
volatileVisibilityThreadSafeTest();
}
/**
* volatile可见性安全测试
*/
public void volatileVisibilityThreadSafeTest() {
close();
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的volatile变量:" + close);
open();
System.out.println(Thread.currentThread().getName() + "(implements Runnable)的volatile变量:" + close);
}
public void close() {
close = true;
}
public void open() {
close = false;
}
}
解析:由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见,从而达到线程安全的目的。那么JMM是如何实现让volatile变量对其他线程立即可见的呢?实际上,当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见。
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤1和步骤2间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
//禁止指令重排优化
private volatile static DoubleCheckLock instance;