阅读 107

java运行时的内存分布与线程同步

前言

在学习线程的时候,肯定会遇到线程同步问题

多个并发线程之间按照某种机制协调先后次序执行,当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作

根据上面的定义我们可以知道,线程同步问题主要是多线程并发情况下如何互斥访问共享资源;在多个线程对同一变量进行读写操作时,如果没有原子性,就可能产生脏数据。实现线程同步有很多的方式,比如同步方法,锁,阻塞队列等。

那么java中的线程是如何访问资源的呢,在学习了一段时间的jvm后,对于线程访问共享资源有了更深的理解

JVM的内存布局

Java虚拟机在运行Java程序时,会把它所管理的内存区域划分为若干个不同的区域,每个区域都有各自的用途;下图是经典的JVM内存布局

jvm内存模型

线程私有区域
  • 程序计数器 (PC): 当前线程所执行的字节码的行号指示器,用于线程切换后能恢复到正确的执行位置

  • Java虚拟机栈: 每个线程都有一个自己的Java栈, 描述了Java方法运行时的内存模型:每个方法在执行时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息;每个方法的从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程

  • 本地方法栈:与JAVA栈类似,区别是使用的对象不一样,本地方法栈是给Native方法使用的,JAVA虚拟机栈是给JAVA方式使用的

线程共享区域
  • 堆区(GC堆): 他是JVM所管理的内存中最大的一块,用于存放new处来的对象实例,被所有线程所共享;由垃圾回收器回收
  • 方法区: 所有线程共享一个内存区;他用于存储已被虚拟机加载的类元信息,静态变量,常量(JDK8后字符串常量已经被移至堆内存),JIT编译后的代码等;Java虚拟机规范规定可以不对方法区进行垃圾回收,当并不是不回收,主要看具体虚拟机的实现,比如可以回收一些废弃常量和无用的类;

多线程访问共享内存

多个线程同时执行同一个方法的时候,出现异常结果的情况:

在没有任何同步机制的情况下,多个线程共享一块内存区域,对其操作;

得到正常结果的情况:
  • 不使用共享内存,每个线程内存空间相互独立;

  • 多线程共享一块内存区域,但是对这块共享区域加锁访问;

例子

1. 在没有任何同步机制的情况下,多个线程共享一块内存区域,对其操作

对静态变量sum进行操作,静态变量保存在线程共享的内存区域中,多个线程可以同时访问

package com.thread;

/**
 * @Author: ranjun
 * @Date: 2019/7/22 14:13
 */
public class AddTest {

    private static int sum = 0;

    /**
     * 对静态变量sum进行累加操作
     * @param n
     * @return
     */
    public static int sum(int n){
        sum = 0;
        for(int i = 0; i <= n; i++){
            sum += i;
            //让出cpu资源,让其他线程执行;
            //可以将下面这段注释掉,如果累加次数足够少时,仍然会得到正确结果(当前线程可以在当前时间片中完成累加操作,并返回值)
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return sum;
    }
}

复制代码

写个main函数,定义四个线程,每个线程都调用上面的静态方法,观察运行结果,基本都是错误的:

package com.thread;

/**
 * @Author: ranjun
 * @Date: 2019/7/22 14:17
 */
public class MainTest {

    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println(Thread.currentThread().getName() +":" +AddTest.sum(100));
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}


复制代码

部分运行结果,都是错的

Thread-3:20153
Thread-1:19953
Thread-2:19953
Thread-0:20153
Thread-1:18510
Thread-3:18510
复制代码

原因:多个线程同时对静态全局变量s进行操作导致;

ps:这里的例子是静态全局变量s,其实有很多种情况会引起结果异常问题,如在main方法中new出了一个对象,new出来的对象是存放在堆中的,多个线程共享,此时如果多线程同时操作该对象的话,也是有可能产生错误结果;

2. 不使用共享内存,每个线程内存空间相互独立

修改静态sum方法,使用局部变量sum,如下:

 public static int sum(int n){
        int sum = 0;
        for(int i = 0; i <= n; i++){
            sum += i;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return sum;
    }
复制代码

运行程序,结果正确:

Thread-3:5050
Thread-1:5050
Thread-2:5050
Thread-0:5050
Thread-0:5050
Thread-1:5050
Thread-2:5050
Thread-3:5050
复制代码
3. 多线程共享一块内存区域,但是对这块共享区域加锁访问
 public synchronized static int sum(int n){
            sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += i;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return sum;
    }
复制代码

运行程序,结果正确:

Thread-1:5050
Thread-3:5050
Thread-2:5050
Thread-0:5050
Thread-2:5050
复制代码

总结

从上面的例子可以看到,多线程对共享资源的操作结果是难以控制的;所以在多线程编程过程中,我们要知道哪些资源是线程共享的,哪些是线程本地私有的,然后合理的对共享资源进行协调。

关注下面的标签,发现更多相似文章
评论