首页 > 技术文章 > Java内存模型

sunliyuan 2020-01-14 20:39 原文

由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。

Java程序执行流程:

 

 

Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),

然后由JVM中的类加载器加载各个类的字节码文件,

加载完毕之后,交由JVM执行引擎执行。

Java内存模型指的就是Runtime Data Area(运行时数据区),即程序执行期间用到的数据和相关信息保存区

1  Java内存模型

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。结构如下图

 

 

1.    PC程序计数器:

l  每个线程对应有一个程序计数器。

l  各线程的程序计数器是线程私有的,互不影响,是线程安全的。

l  程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次按照中断时的指令地址继续执行

1.    Java栈JavaStack(虚拟机栈JVM Stack):

l  每个线程会对应一个Java栈;

l  每个Java栈由若干栈帧组成;

l  每个方法对应一个栈帧;

l  栈帧在方法运行时,创建并入栈;方法执行完,该栈帧弹出栈帧中的元素作为该方法返回值,该栈帧被清除;

l  栈顶的栈帧叫活动栈,表示当前执行的方法,才可以被CPU执行;

l  线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;

l  栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

 

 

1.    方法区MethodArea

l  方法区是Java堆的永久区(PermanetGeneration)

l  方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,

l  方法区是被Java线程共享的

l  方法区要使用的内存超过其允许的大小时,会抛出OutOfMemoryError: PremGen space的错误信息。

1.    常量池ConstantPool:

l  常量池是方法区的一部分。

l  常量池中存储两类数据:字面量和引用量。

字面量:字符串、final变量等。

引用量:类/接口、方法和字段的名称和描述符,

l  常量池在编译期间就被确定,并保存在已编译的.class文件中

1.    本地方法栈Native Method Stack:

l  本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。

l  本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

1.1.  Java内存模型工作示意图

 

 

1)         首先类加载器将Java代码加载到方法区

2)         然后执行引擎从方法区找到main方法

3)         为方法创建栈帧放入方法栈,同时创建该栈帧的程序计数器

4)         执行引擎请求CPU执行该方法

5)         CPU将方法栈数据加载到工作内存(寄存器和高速缓存),执行该方法

6)         CPU执行完之后将执行结果从工作内存同步到主内存

 

1.   多线程特性

多线程编程要保证满足三个特性:原子性、可见性、有序性

.  原子性

原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

.  可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。显然,对于单线程来说,可见性问题是不存在的。

.  有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

1.   多线程控制类

为了保证多线程的三个特性,Java引入了很多线程控制机制

l  ThreadLocal:线程本地变量

l  原子类:保证变量原子操作

l  Lock类:保证线程有序性

l  Volatile关键字:保证变量可见性

.  ThreadLocal

1.1.1.    作用

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现

l  在ThreadLocal类中定义了一个ThreadLocalMap,

l  每一个Thread都有一个ThreadLocalMap类型的变量threadLocals

l  threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本

l  ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本

l  ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

public class ThreadLoacalDemo {
	static class Bank {
		private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
			@Override
			protected Integer initialValue() {
				return 0;
			}
		};

		// 取款
		public Integer get() {
			return threadLocal.get();
		}

		public void set(Integer maney) {
			threadLocal.set(threadLocal.get() + maney);
		}
	}

	static class Transfer implements Runnable {
		private Bank bank;

		public Transfer(Bank bank) {
			this.bank = bank;
		}

		@Override
		public void run() {
			for (int i = 0; i < 10; i++) {
				bank.set(10);
				System.out.println(Thread.currentThread().getName() + "账户余额" + bank.get());
			}
		}
	}

	public static void main(String[] args) {
		Bank bank = new Bank();
		Transfer transfer=new Transfer(bank);
		Thread thread1=new Thread(transfer,"客户1");
		Thread thread2=new Thread(transfer,"客户2");
		
		thread1.start();
		thread2.start();

	}

}

  

 

l  在ThreadLocal类中定义了一个ThreadLocalMap,

l  每一个Thread都有一个ThreadLocalMap类型的变量threadLocals

l  threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本

l  ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本

l  ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

 

1.1.  原子类

Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:

l  原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong

l  原子更新数组:AtomicIntegerArray、AtomicLongArray

l  原子更新引用:AtomicReference、AtomicStampedReference等

l  原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

i++并不是原子操作,而是由三个操作构成:

tp1 = i;
tp2 = tp1+1;
i = tp2;

  所以单线程i的值不是有问题,但多线程下就会出错.

演示代码:

public class ThreadAtomicDemo {
	static private int n;// 执行n++操作的变量

	public static void main(String[] args) throws InterruptedException {
		int j = 0;
		while (j < 100) {
			n = 0;
			Thread thread1 = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 1000; i++) {
						n++;
					}

				}
			});

			Thread thread2 = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 1000; i++) {
						n++;
					}

				}
			});

			thread1.start();
			thread2.start();
			thread1.join();// 加入主线程
			thread2.join();
			j++;
			System.out.println("n的最终值是" + n);
		}
	}
}

执行结果如下:发现n的最终值可能不是2000

 

 

原子类解决非原子性操作:

 

 

static AtomicInteger atomicInteger;

	public static void main(String[] args) throws InterruptedException {
		int j = 0;
		while (j < 100) {
			atomicInteger = new AtomicInteger(0);// 创建原子整数,初始值0
			Thread thread1 = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
						atomicInteger.getAndIncrement();
					}
				}
			});

			Thread thread2 = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
						atomicInteger.getAndIncrement();
					}
				}
			});
			
			 thread1.start();
			 thread2.start();
			 thread1.join();// 加入主线程
			 thread2.join();
			 j++;
			 System.out.println("n的最终值是" + atomicInteger.get());
		}
	}

  

 

 上图为cas的原理:

1.1.1.    CAS的ABA问题及解决

当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过

 

 解决方法:

 

  

static AtomicStampedReference<Integer> atomicStampedReference; 
	
	public static void main(String[] args) throws InterruptedException {
		int j=0;
		while(j<100){
			atomicStampedReference=new AtomicStampedReference<Integer>(0, 0);
			Thread thread1=new Thread(){
				@Override
				public void run(){
					for(int i=0;i<10000;i++){
						int stamp;
						Integer reference;
						do {
						   stamp=atomicStampedReference.getStamp();
						   reference=atomicStampedReference.getReference();	
						} while (!atomicStampedReference.compareAndSet(
								reference,reference+1, stamp, stamp+1 ));
					}
				}
			};
			
			Thread thread2=new Thread(){
				@Override
				public void run(){
					for(int i=0;i<10000;i++){
						int stamp;
						Integer reference;
						do {
						   stamp=atomicStampedReference.getStamp();
						   reference=atomicStampedReference.getReference();	
						} while (!atomicStampedReference.compareAndSet(
								reference,reference+1, stamp, stamp+1 ));
					}
				}
			};
		
			 thread1.start();
			 thread2.start();
			 thread1.join();// 加入主线程
			 thread2.join();
			 j++;
			 System.out.println("n的最终值是" + atomicStampedReference.getReference());
		}
	}

  注意:采用AtomicStampedReference会降低性能,慎用

1.1.1.    Lock接口关系图

 

 

Lock和ReadWriteLock是两大锁的根接口

Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。

1.1.1.    可重入锁

不可重入锁,即线程请求它已经拥有的锁时会阻塞。

可重入锁,即线程可以进入它已经拥有的锁的同步代码块。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
	public static void main(String[] args) {
		ReentrantLock lock=new ReentrantLock();
		for(int i=0;i<10;i++){
			lock.lock();
			System.out.println("枷锁次数"+(i+1));
		}
		
		for(int i=0;i<10;i++){
			try{
				System.out.println("解锁次数:"+(i+1));
			}finally {
				lock.unlock();
			}
		}
	}
}

  

1.1.1.    读写锁

读写锁,即可以同时读,读的时候不能写;不能同时写,写的时候不能读。

 

public class ReadWriteLockDemo {
	private Map<String, String> map=new HashMap<String,String>(); //操作的map 对象
	private ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
	
	private ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();//读操作的锁
	private ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();//写操作的锁
	
	public String get(String key){
		readLock.lock();//读操作枷锁
		try {
			System.out.println(Thread.currentThread().getName()+"读操作已枷锁,开始读操作...");
			Thread.sleep(300);
			return map.get(key);
		} catch(Exception e){
			return null;
		}finally{
			System.out.println(Thread.currentThread().getName()+"读操作已解锁,读操作结束....");
			readLock.unlock();
		}
	}
	
	public void put(String key,String value){
		writeLock.lock();
		try {
			System.out.println(Thread.currentThread().getName()+"写操作已经枷锁,正在执行写操作");
			Thread.sleep(3000);
			map.put(key, value);
		}catch(Exception e){
			e.printStackTrace();
		} finally {
			System.out.println(Thread.currentThread().getName()+"写操作解锁,写操作执行完成");
			writeLock.unlock();
		}
	}
	
	public static void main(String[] args) {
		final ReadWriteLockDemo readWriteLockDemo=new  ReadWriteLockDemo();
		readWriteLockDemo.put("key1", "value1");
		new Thread("读线程1"){
			public void run(){
			System.out.println(readWriteLockDemo.get("key1"));
			}
		}.start();
		new Thread("读线程2"){
			public void run(){
			System.out.println(readWriteLockDemo.get("key1"));
			}
		}.start();
		new Thread("读线程3"){
			public void run(){
			System.out.println(readWriteLockDemo.get("key1"));
			}
		}.start();
	}
}

  

Volatile关键字

1.1.1.    作用

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)

l  禁止进行指令重排序。(保证变量所在行的有序性)

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

1.1.1.    应用场景

基于volatile的作用,使用volatile必须满足以下两个条件:

l  对变量的写操作不依赖于当前值

l  该变量没有包含在具有其他变量的不变式中

常见应用场景如下:

状态量标记:

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

  

volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context); 

双重校验:

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
           synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

  

 

推荐阅读