盒子
盒子
Posts List
  1. 一.问题列表
  2. 二. Java虚拟机内存模型与堆栈
  3. 三.Java程序运行期间堆栈情况
  4. 四.进程与线程在Java中的表现
    1. 1.进程与线程概述
    2. 2.进程的生成
    3. 3.线程的生成
      1. 1. Thread 产生线程
      2. 2.Runnable 是怎么回事?
  5. 四. 多个线程共享对象
  6. 五.ThreadLocalStorage
    1. 1.ThreadLocalStorage 是什么
    2. 2.ThreadLocal 实现思路

深入Java内存模型

在看android中消息机制(Handler , MessageQueue , Message , Looper)原理过程中,发现ThreadLocal的实现有些不明白,查询资料后发现对于android多线程执行原理理解很模糊,这是我写这篇文章的起源。

要理解Java的多线程原理,必须要明白Java内存模型,我给自己提了一些问题,理解这些问题需要的说明的内容比较多,有些内容限于篇幅我引用了一些写的很清晰作者的文章。如有侵权,请联系我。

问题总结起来可以归类为两部分:

  • 堆与栈的理解
  • 多线程的理解

下面是详细的问题列表

一.问题列表

  • 问题一:什么是堆,什么是栈,什么是堆栈,他们和内存是什么关系?
  • 问题二:java程序在运行的时候,哪些内容放在堆里,哪些内容放在栈里,方法放在哪里,常量放在哪里,class是什么?
  • 问题三:什么是进程,什么是线程?Java中什么是一个进程?能有多个进程吗?new Thread 和 Runnable是什么关系?
  • 问题四:多个线程为什么都可以访问类的成员变量,什么是线程栈,他是我们常说的栈吗?
  • 问题五:什么是ThreadLocalStorage, 的作用是什么, 实现原理 是什么?

二. Java虚拟机内存模型与堆栈

JVM内存模型中的堆与栈,和数据结构中的堆(先进先出)和栈(后进先出)没有什么关系。

JVM内存模型图如下,其中红色区域为线程共享,灰色区域为线程私有:

内存模型共分为5部分,这5部分分工合作,保证JVM的正常运行:

  • 栈:栈是一块后进先出(LIFO)的存储空间,它存储方法执行过程中的现场,JVM中称为栈帧,每个新方法被执行,都会产生一个栈帧。由于它保存的是方法执行现场,因此它是每个线程私有的。线程之间互相不能访问。栈的大小在JVM启动的时就确定了,当方法出现循环嵌套调用导致栈空间不足时,会抛出StackOverFlow异常。

栈形态如下:

  • 堆: 堆是JVM用于存储对象实例的内存空间。JVM在启动的时候,可以通过设置参数设置堆空间大小。它被所有线程共享。当对象过多导致在堆上空间不足,且有新的内存申请时,会抛出OutOfMemory异常。
  • 本地方法栈:本地方法栈的作用类似于栈,只是保存的内容为本地方法(native标记)的内容。它也是线程私有的。
  • 程序计数器:存储下一条指令的存储单元地址,它也是线程私有的。如果执行的是本地方法,则保存的是undefined
  • 方法区:方法区保存编译后的类的信息(类名,字段,方法),静态变量,常量,编译后代码等。方法区也会出现内存溢出,逻辑上方法区也属于堆的一部分,因此也会抛出OutOfMemory异常。

方法区的形态如下:


总结一下:

  • 通过new 产生的对象都放在堆中,包括对象的属性的值,被所有线程共享。
  • 每个方法执行都会以栈帧的形式存储在栈中,栈以LIFR的形势维护多个栈帧。它是线程私有的
  • 当前方法(栈最顶部的栈帧)执行的下一条指令的地址保存在程序计数器中。它是线程私有的
  • 类(包括属性和方法)本身以特定的组织形式被类加载器加载并存放在方法区中,它也是堆的一部分,被所有线程共享。
  • native方法放在本地方法栈,它是线程私有的。

三.Java程序运行期间堆栈情况

Java程序被编译后是以class文件的形式组织的,class文件包含了类的属性和方法的描述,它被加载到内存后放在了方法区。

关于class的具体组织形式,在JVM方法调用详情,引用以下文章可以参考:

四.进程与线程在Java中的表现

1.进程与线程概述

进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位,CPU的单个执行时间片只能运行一个线程的逻辑。

进程的出现为了提高多任务环境下的CPU和内存的使用率
线程的出现为了解决单个任务分段运行问题。

进程间相互独立,线程依附于进程而存在。同一个进程的所有线程共享进程的资源(地址空间)。

新建线程占用的资源非常少,单个线程包含的资源包括 线程栈,程序计数器,线程id等。

阮一峰关于进程与线程的通俗理解

2.进程的生成

一般来讲,在Java中,启动一次JVM会生成一个进程。例如包含

public static void main(String[] args){
}

方法的类在运行的时候,会产生一个进程。

在Android中,每个app启动都会生成一个进程,包含android:process的除外。

3.线程的生成

1. Thread 产生线程

一般通过new thread 或者继承自 Thread的类实例化来新建线程。

class MyThread extends Thead {
private int shareInt = 20;
public void run(){
shareInt -= 1;
System.out.println("" + shareInt);
}
}

此处的shareInt 属于线程私有属性,其他线程无法访问,因此无论新生成多少个Thread ,输入shareInt都为19。

2.Runnable 是怎么回事?

本质上来讲,Runnable仅仅是一个接口定义,它与线程的产生没有任何关系,实现了Runnable接口的类会被加载到方法区供所有线程共享使用。

Runnable可以看成是一个执行片段的包装。最终产生线程还是通过
new Thread。由于Runnable可以被多个线程包装使用,因此它非常适合用于线程间共享数据。MyRun对象实际上就是线程的共享数据。

//MyRun.java
class MyRun implements Runnable{
private int shareInt = 20;
public void run(){
shareInt -= 1;
System.out.println("" + shareInt);
}
}
//Test.java
class Test{
public static void main(String[] args){
MyRun r = new MyRun();
new Thread(r).start();
new Thread(r).start();
}
}

shareInt 经过两个线程操作,最终值为18。

四. 多个线程共享对象

还是使用上个例子来说明这个问题。

//MyRun.java
class MyRun implements Runnable{
private int shareInt = 20;
public void run(){
shareInt -= 1;
}
}
//Test.java
class Test{
public static void main(String[] args){
MyRun r = new MyRun();
new Thread(r).start();
new Thread(r).start();
}
}

此时,JVM内存中的数据大致如下:

两个线程使用同一个对象myRun,也能访问MyRun的所有属性和方法。

此处的线程1 和 线程2,分别拥有各自的线程栈,也就是Java中常说的栈。

一般来说堆中的所有对象能被所有线程访问,为了解决冲突问题,Java设计了一套机制来处理。相关概念包括 对象锁,wait ,sleep ,syncronized等,这里不再展开。

五.ThreadLocalStorage

1.ThreadLocalStorage 是什么

顾名思义,ThreadLocalStorage简称TLS是线程本地存储数据的一种方案,它避开线程共享资源坑,为每个线程提供一份资源的拷贝。

2.ThreadLocal 实现思路

ThreadLocal可以在多个线程中互不干扰地存储和修改数据。它实现的主体思路为:公共对象ThreadLocal通过操纵当前线程的Values实现数据的存储与获取。

android中的Thread中,有个属性ThreadLocal.Values,它是每个Thread私有的属性。

class Thread implements Runnable{
...
ThreadLocal.Values localValues;
...
}

ThreadLocal.java中的部分代码

package java.lang;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocal<T> {
public ThreadLocal() {}
/**
* 获取当前线程的数据
*/
@SuppressWarnings("unchecked")
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
...
/**
* 为当前线程设置数据
*/
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
....
/**
* 获取每个线程的Localvalues
*/
Values values(Thread current) {
return current.localValues;
}
/** 自身的弱引用 */
private final Reference<ThreadLocal<T>> reference
= new WeakReference<ThreadLocal<T>>(this);
/** Hash counter. */
private static AtomicInteger hashCounter = new AtomicInteger(0);
/**
* 每个线程获取唯一的Hash值
*/
private final int hash = hashCounter.getAndAdd(0x61c88647 * 2);
/**
* Per-thread map of ThreadLocal instances to values.
*/
static class Values {
....
}
}

get和set方法处理的对象都是当前线程的Values,他们是线程私有的,因此可以保证在不同线程中,通过操作同一个ThreadLocal对象,存储各自的数据。

网上摘了个使用ThreadLocal的例子如下:

private ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();
mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
new Thread("Thread#1") {
@Override
public void run() {
mBooleanThreadLocal.set(false);
Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
};
}.start();
new Thread("Thread#2") {
@Override
public void run() {
Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
};
}.start();

最终输出:

D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true
D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false
D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null

支持一下
扫一扫,支持牛头码农