1. 程式人生 > 程式設計 >深入理解java併發程式設計基礎篇(二)-------執行緒、程式、Java記憶體模型

深入理解java併發程式設計基礎篇(二)-------執行緒、程式、Java記憶體模型

一、前言

  通過前面的學習,我們瞭解到一些關於併發程式設計的一些基本概念,這一篇將繼續總結以及複習基礎篇的內容。

二、程式以及執行緒

2.1 什麼是程式?

  程式是程式的一次執行過程,是系統執行程式的基本單位,因此程式是動態的。系統執行一個程式即是一個程式從建立,執行到消亡的過程。

  在 Java 中,當我們啟動 main 函式時其實就是啟動了一個 JVM 的程式,而 main 函式所在的執行緒就是這個程式中的一個執行緒,也稱主執行緒。

  如下圖所示,在 windows 中通過檢視工作管理員的方式,我們就可以清楚看到 window 當前執行的程式:

2.2 什麼是執行緒?

  執行緒與程式相似,但執行緒是一個比程式更小的執行單位。一個程式在其執行的過程中可以產生多個執行緒。與程式不同的是同類的多個執行緒共享程式的堆和方法區資源,但每個執行緒有自己的程式計數器、虛擬機器器棧和本地方法棧,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程式小得多,也正因為如此,執行緒也被稱為輕量級程式。
  那麼下面我們會從jvm角度來分析執行緒與程式的關係。

2.3 圖解執行緒與程式的關係

下面是簡略版的圖解:


  從上圖可以看出:一個程式中可以有多個執行緒,多個執行緒共享程式的堆和方法區資源,但是每個執行緒有自己的程式計數器、虛擬機器器棧 和 本地方法棧。

總結: 執行緒是程式劃分成的更小的執行單位。執行緒和程式最大的不同在於基本上各程式是獨立的,而各執行緒則不一定,因為同一程式中的執行緒極有可能會相互影響。執行緒執行開銷小,但不利於資源的管理和保護;而程式正相反。

三、Java記憶體模型

  JMM(java記憶體模型),由於併發程式要比序列程式複雜很多,其中一個重要原因是併發程式中資料訪問一致性和安全性將會受到嚴重挑戰。如何保證一個執行緒可以看到正確的資料呢?這個問題看起來很白痴。對於序列程式來說,根本就是小菜一碟,如果你讀取一個變數,這個變數的值是1,那麼你讀取到的一定是1,就是這麼簡單的問題在並行程式中居然變得複雜起來。事實上,如果不加控制地任由執行緒胡亂並行,即使原本是1的數值,你也可能讀到2。因此我們需要在深入瞭解並行機制的前提下,再定義一種規則,保證多個執行緒間可以有小弟,正確地協同工作。而JMM也就是為此而生的。

  JMM關鍵技術點都是圍繞著多執行緒的原子性、可見性、有序性來建立的。我們需要先了解這些概念。

3.1 原子性

  原子性是指操作是不可分的,要麼全部一起執行,要麼不執行。在java中,其表現在對於共享變數的某些操作,是不可分的,必須連續的完成。比如a++,對於共享變數a的操作,實際上會執行3個步驟:

1.讀取變數a的值,假如a=1

2.a的值+1,為2

3.將2值賦值給變數a,此時a的值應該為2

這三個操作中任意一個操作,a的值如果被其他執行緒篡改了,那麼都會出現我們不希望出現的結果。所以必須保證這3個操作是原子性的,在操作a++的過程中,其他執行緒不會改變a的值,如果在上面的過程中出現其他執行緒修改了a的值,在滿足原子性的原則下,上面的操作應該失敗。

java中實現原子操作的方法大致有2種:鎖機制、無鎖CAS機制,後面的章節中會有介紹。

3.2 可見性

  可見性是值一個執行緒對共享變數的修改,對於另一個執行緒來說是否是可以看到的。

  首先看一下Java執行緒記憶體模型:


  執行緒需要修改共享資源X,需要先把X從主記憶體複製一份到執行緒的工作記憶體,在自己的工作記憶體中修改完畢之後,再從工作記憶體中回寫到主記憶體。如果執行緒對變數的操作沒有刷寫回主記憶體的話,僅僅改變了自己的工作記憶體的變數的副本,那麼對於其他執行緒來說是不可見的。而如果另一個變數沒有讀取主記憶體中的新的值,而是使用舊的值的話,同樣的也可以列為不可見。

共享變數可見性的實現原理:

1.執行緒A在自己的工作記憶體中修改變數之後,需要將變數的值重新整理到主記憶體中
2.執行緒B要把主記憶體中變數的值更新到工作記憶體中

關於執行緒可見性的控制,可以使用volatile、synchronized、鎖來實現,後面章節會有詳細介紹。

3.3 有序性

  有序性指的是程式按照程式碼的先後順序執行。 為了效能優化,編譯器和處理器會進行指令衝排序,有時候會改變程式語句的先後順序。

比如下面的例子:

	int a = 1;  //1
	int b = 2;  //2
	int c = a + b;  //3
複製程式碼

經過編譯器以及處理器優化後,有可能會變成下面的順序:

    int b = 2;  //1
	int a = 1;  //2
	int c = a + b;  //3
複製程式碼

上面這個例子調整了程式碼執行順序,但是並不會影響程式執行的最後結果。

那麼我們再來看看一個例子(單例是實現--雙重加鎖實現方式):

package com.MyMineBug.demoRun.test;

public class Singleton {
	static Singleton instance;

	static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null)
					instance = new Singleton();
			}
		}
		return instance;
	}
}
複製程式碼

未被編譯器優化的操作:

指令1:分配一款記憶體H

指令2:在記憶體H上初始化Singleton物件

指令3:將H的地址賦值給instance變數

編譯器優化後的操作指令:

指令1:分配一塊記憶體W

指令2:將W的地址賦值給instance變數

指令3:在記憶體W上初始化Singleton物件

如果此刻有多個執行緒執行這段程式碼,會出現意想不到的結果。

那麼單例模式的建立怎樣才是最佳的呢?我們再後續討論。

  如果覺得還不錯,請點個贊!!!

  Share Technology And Love Life