什么是JAVA 内存模型
Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。具体说来,JVM中存在一个主存区(Main Memory或Java HeapMemory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。具体的如下图所示:
JMM描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存中读取出变量这样的底层细节。
所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中变量的一份拷贝)。
JMM的有两条规定
1、线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
2、不同的线程之间无法直接访问其他线程工作内存中的变量,线程变量值的传递需要通过主内存来完成。
内存可见性
可见性:是指一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
共享变量可见性实现的原理:线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤:
1、把工作内存1中更新过的共享变量的值刷新到主内存中;
2、把主内存中最新的共享变量的值更新到工作内存2中。
指令重排序:代码书写的顺序与实际的执行顺序可能不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。
1、编译器优化的重排序(编译器优化);
2、指令级并行重排序(处理器优化);
3、内存系统的重排序(处理器优化)。
而导致共享变量在线程间不可见的主要原因:
1、线程的交叉执行;
2、重排序结合线程的交叉执行;
3、共享变量更新后的值没有在工作内存与主内存之间及时更新。
要实现共享变量的可见性,必须保证两点:
1、线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
2、其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中。
Synchronized
Synchronized的功能包括:原子性(同步) 与 可见性。
JMM关于synchronized的两条规定:
1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要使用的是同一把锁)。
线程解锁前对共享变量的修改在下次加锁时对其他线程可见。
线程执行互斥代码的过程
1、获得互斥锁
2、清空工作内存
3、从主内存拷贝变量的最新副本到工作内存
4、执行互斥代码
5、将更改后的共享变量的值刷新到主内存
6、释放互斥锁
synchronized实现可见性
1、锁内部的代码互斥,能够保证线程不会交叉执行,重排序也一样,由于加锁,线程之间对共享变量的执行总是有序的。
2、由JMM关于synchronized的两条规定保证,synchronized具有可见性功能。
程序实例:
public class SynchronizedTest {
public static void main(String[] args) {
final Service service = new Service();
// 线程a
new Thread(new Runnable() {
@Override
public void run() {
service.runMethod();
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程b
new Thread(new Runnable() {
@Override
public void run() {
service.stopMethod();
}
}).start();
System.out.println("已经发起了停止的命令了!");
}
}
class Service {
// 共享变量
private boolean isContinueRun = true;
public void runMethod() {
String anyString = new String();
while(isContinueRun) {
synchronized (anyString) {
}
}
System.out.println("线程停止!!");
}
public void stopMethod() {
isContinueRun = false;
}
}
Volatile
关键字volatile的主要作用就是使变量在多个线程间可见。深入来说:通过加入内存屏障和禁止重排序优化来实现的。
1、对volatile变量执行写操作时,会在写操作后加入一条store屏障指令;
2、对volatile变量执行读操作时,会在读操作后加入一条load屏障指令。
首先来看如下一个实例:
public class VolatileTest1 {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已近给 isRunning 赋值为false了!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void run() {
System.out.println("进入run了!!");
while(isRunning) {
}
System.out.println("线程被停止了!!");
}
}
程序运行结果:
代码:“System.out.println("线程被停止了!!");”一直无法得到执行。
在启动RunThread.java线程时,变量privateboolean isRunning = true;存在于公共堆栈以及线程的私有堆栈中。JVM为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true。而代码thread.setRunning(false);虽然被执行,更新的确实公共堆栈中的isRunning变量的值false,所以线程一直死循环状态。
这个问题就是私有堆栈(线程工作内存)中的值和公共堆栈(主内存)中的值不同步造成的。解决这样的问题就要使用volatile关键字,它只要的作用就是当前线程访问isRunning这个变量时,强制性从公共堆栈中进行取值。
public class VolatileTest2 {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已近给 isRunning 赋值为false了!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class RunThread extends Thread {
volatile private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void run() {
System.out.println("进入run了!!");
while(isRunning) {
}
System.out.println("线程被停止了!!");
}
}
}
使用volatile关键字时的内存结构如图:
线程写volatile变量的过程:
1、改变线程工作内存中volatile变量副本的值
2、将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
1、从主内存中读取volatile变量的最新值到线程的工作内存中
2、从工作内存中读取volatile变量的副本
volatile不能保证volatile变量复合操作的原子性
例如number++操作分为:
1、读取number的值
2、将number的值加1
3、写入最新的number的值
Volatile非原子性的特征
public class VolatileTest3 {
public static void main(String[] args) {
MyThread[] myThreadArray = new MyThread[100];
for (int i = 0; i < 100; i++) {
myThreadArray[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
myThreadArray[i].start();
}
}
static class MyThread extends Thread {
volatile public static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
public void run() {
addCount();
}
}
}
无法保证最后的结果加到10000
保证number++自增操作的原子性
1、使用synchronized关键字
2、使用ReentrantLock(java.until.concurrent.locks包下)
3、使用AutomicInterger(java.util.concurrent.atomic包下)
/**
*
* @Description: 注意一定要添加static关键字,这样synchronized 与 static锁的内容就是MyThread.class类了,也就达到了同步的效果
*
*/
static class MyThread extends Thread {
volatile public static int count;
synchronized private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
public void run() {
addCount();
}
}
volatile使用场合
要在多线程中安全的使用volatile变量,必须同时满足:
1、对变量的写入操作不依赖其当前值
不满足:number++,conut = conut *5等
满足:boolean变量,记录温度变化的变量等
2、该变量没有包含在具有其他变量的不等式中
不满足:不等式 low < up
synchronized与volatile的比较
1、volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
2、从内存可见性角度看,volatile读相当于加锁,volatile写相当于解锁。
3、synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
4、关键字volatile解决的是变量在多个线程之间的可见性问题,而synchronized关键字解决的是多个线程之间访问资源的同步性。