java運行時的內存分佈與線程同步

前言

在學習線程的時候,確定會遇到線程同步問題java

多個併發線程之間按照某種機制協調前後次序執行,當有一個線程在對內存進行操做時,其餘線程都不能夠對這個內存地址進行操做,直到該線程完成操做編程

根據上面的定義咱們能夠知道,線程同步問題主要是多線程併發狀況下如何互斥訪問共享資源;在多個線程對同一變量進行讀寫操做時,若是沒有原子性,就可能產生髒數據。實現線程同步有不少的方式,好比同步方法,鎖,阻塞隊列等。bash

那麼java中的線程是如何訪問資源的呢,在學習了一段時間的jvm後,對於線程訪問共享資源有了更深的理解多線程

JVM的內存佈局

Java虛擬機在運行Java程序時,會把它所管理的內存區域劃分爲若干個不一樣的區域,每一個區域都有各自的用途;下圖是經典的JVM內存佈局 併發

jvm內存模型

線程私有區域
  • 程序計數器 (PC): 當前線程所執行的字節碼的行號指示器,用於線程切換後能恢復到正確的執行位置jvm

  • Java虛擬機棧: 每一個線程都有一個本身的Java棧, 描述了Java方法運行時的內存模型:每一個方法在執行時都會建立一個棧幀用於儲存局部變量表、操做數棧、動態連接、方法出口等信息;每一個方法的從開始調用到執行完成的過程,就是棧幀從入棧到出棧的過程ide

  • 本地方法棧:與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
複製代碼

總結

從上面的例子能夠看到,多線程對共享資源的操做結果是難以控制的;因此在多線程編程過程當中,咱們要知道哪些資源是線程共享的,哪些是線程本地私有的,而後合理的對共享資源進行協調。

相關文章
相關標籤/搜索