这里是通过在主线程中终止一个用户线程的操作的例子。但在本内容之前需要首先了解Java内存模型。
1. java内存模型:指定了虚拟机和Java内存RAM是如何运作的
这里是关于java内存模型的详细资料,但全部是英文的,如果不想细看,可以看我的简略理解.
1). 逻辑角度的Java内存模型。
java内存模型把内存分为线程栈和堆。
栈:栈中存有该线程方法调用信息;被该线程调用方法的局部变量;所有基本类型(boolean, byte,short, char,int,long,float,double)的局部变量都存在栈上。
堆:堆中包含应用中所有线程创建的所有对象。
具体而言:
a).基本类型的局部变量:这种局部变量完全保存在线程栈上。
b).对象引用类型的局部变量:对象的引用(局部变量)被存在线程栈上,但对象本身存放位置是堆上。
c).对象所包含方法的局部变量:这些方法的局部变量存放在线程栈上。
d).对象的成员变量和对象本身都被存放在堆上。无论成员变量是基本数据类型还是引用数据类型都是如此。
e).静态类型的类变量和类的定义本身都被存放在堆上。
如下代码段所示:
//上述内容对应的代码段
public class MyRunnable implements Runnable() {
public void run() {
methodOne();
}
public void methodOne() {
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject, object 3
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member1 = 67890;
}
假设:2个线程在同时执行run()方法,那么上图中显示的内存结构图就是执行过程中的内存中数据结果图了。
run()方法调用了methodOne(), methodOne() 调用了methodTwo()。methodOne()内声明了一个基本类型局部变量(int localVariable1)和一个对象引用类型局部变量(MySharedObject localVariable2)。
每个执行methodOne()的线程都会在各自的线程栈上创建其对应的localVariable1 和localVariable2的一份拷贝。其中每个线程的localVariable1是完全独立的,每个线程的localVariable1在各自线程栈上单独存在。一个线程不能看到另一个线程对于其自身拷贝所做的修改。对于localVariable2也有一定的相似之处,两个线程都有其自身关于localVariable2的拷贝,但是这个拷贝只是对象引用地址,其指向的对象内容是一样的–都是指向MySharedObject对象的,而MySharedObject对象是存在堆上的,对应于Object3.
这里MySharedObject类包含有2个成员变量,该成员变量和该对象本身都存在于堆上。(其实,下面基本类型成员变量member1, member2也存在于堆上). 这两个成员变量指向了2个整形对象,对应于object2和object4.
注意,methodTwo()中创建了局部变量localVariable1,这个局部变量是整形数据的引用,因而在2个线程中是有一份单独的存在,而且每次执行时创建的对象不同的,所以对应的是内存中不同的区域object1 和 object5。
2). 计算机硬件内存模型
计算机内存模型主要分为三个部分:CPU寄存器、CPU cache缓存和 Main Memory(RAM)内存。
3). JVM模型和硬件架构的对应
硬件内存:不区分堆内存还是栈内存,都是位于主存中的,部分存在于CPU 寄存器或cache中的。
2. JVM和实际硬件模型之间存在的问题
1). Visibility: 在多线程环境中,如果多个线程在共享同一个对象时,如果没有正确地使用volatile关键字或使用任何同步机制(synchronizition自带volatile属性),那么一个线程对于对象的修改可能对于其他的线程是不可见的invisibile的。
如图所示:假设共享对象shared object存在主存中,如果此时一个线程把该数据读入其cache中,然后对该数据进行修改,只要该对象还没有flushed入主存,那么该共享对象的值对其他线程是不可见的。所以,可能线程thread1将obj.count 修改为2了,但是线程thread2读到的结果依然是obj.count=1.
这就是所谓的Visibility问题,因为其他线程对于数据的修改并没有flush到主存中。
这个问题可以用volatile关键字解决:volatile关键字保证了被修饰变量直接从主存中被读入,一旦更新后会回写到主存中。
2). Race Condition: 如果多个线程共享对象,并且多个线程更新共享对象的值时,那么竞争条件就出现了。
如果多线程正常运行的话,每个线程对主存中的数据进行+1操作,那么在两次操作后结果为增2.但是这里的问题是:因为++操作并不是原子操作,所以可能出现如图所示尴尬结果, i++操作实际执行过程分为三步, :
a. 获取i的值;
b. 执行i+1操作;
c. 将更新的结果赋值给i;
由于不是原子操作,所以可能第一步时获取的同是i=1, 然后分别执行结果就出现了运行结果是+1的情况。
3. volatile Demo
package com.fqy.cave;
import java.util.Scanner;
class Processor extends Thread {
// volatile keyword here is to ensure visibility
private volatile boolean running = true;
@Override
public void run() {
while (running) {
System.out.println(Thread.currentThread().getName() + " Hello, Concurrent Programming!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void shutdown() {
running = false;
}
}
/*
* 多线程共享同一对象时,一个线程对线程所做的修改可能对于其他线程不可见! 本例中有2个线程;主线程和用户线程p, 如果没有volatile关键字,
* 主线程对于boolean变量running的操作可能对于用户线程不可见。
*/
public class Cache {
public static void main(String[] args) {
Processor p = new Processor();
p.start();
System.out.println("Press enter to stop executing...");
Scanner scanner = new Scanner(System.in);
scanner.nextLine();
scanner.close();
p.shutdown();
System.out.println(Thread.currentThread().getName() + " terminated another thread!");
}
}
//Running result:
Thread-0 Hello, Concurrent Programming!
Press enter to stop executing...
Thread-0 Hello, Concurrent Programming!
Thread-0 Hello, Concurrent Programming!
main terminated another thread!