談到并發(fā),首先要提到的就是安全,所有的并發(fā)編程的前提都是安全。在java中,
- 什么是線程安全性?
- 在多線程并發(fā)訪問一個對象或者類時,他始終都能表現(xiàn)出正確的行為,也就是說,我們在調(diào)用時不需要做額外的同步。
- 線程安全的幾個特點
- 原子性
原子性,通俗來說就是幾個操作在操作過程中,是不可以被其他線程干擾,舉例來說
因為++操作在java中是非原子的,它包含三個操作:讀-改-寫,那么在使用該類的時候我們就無法保證原子性public class Demo { private int count = 0; //在并發(fā)環(huán)境下,多個線程同時執(zhí)行以下方法,可能會發(fā)生不安全操作 public void add() { ++count; } }- 可見性
可見性簡單來說就是當(dāng)一個線程修改了某個共享變量的值,這個更新能夠立刻被其他線程所知曉,具體的原理我們在JMM中詳細說。 - 有序性
有序性,是保證線程內(nèi)語義是串行的,禁止指令重排。舉個例子
當(dāng)發(fā)生指令重排時,語句3先于2執(zhí)行,則結(jié)果發(fā)生錯誤。尤其是在-server模式下,處于對性能的考慮,虛擬機會做出一些優(yōu)化。int a ; a = 5; a = 6; int b = a; int c = a + b;
但是指令重排是有原則的,一下情況不發(fā)生重排,我列舉一下:- Happen-Benfore原則
- 程序順序原則:一個線程內(nèi)語義串行性
- volatile原則:volatile變量的寫先發(fā)生于讀,因此保證了可見性
- 鎖原則:解鎖必然發(fā)生于隨后的加鎖前
- 傳遞性:a先于b,b先于c,則a先于c
- 線程的start方法先于他的每一個操作
- 線程的所有操作先于線程的終結(jié)
- 對象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于finalize方法
- 原子性
java內(nèi)存模型
-
計算機內(nèi)存模型
我們知道java是一種可移植的語言,它是平臺無關(guān)性的,所以這就要求JMM在任何平臺下都能達到一致的內(nèi)存訪問效果。- 首先我們來了解一下在計算機中硬件中是如何實現(xiàn)并發(fā)的,在計算機中,IO操作的速度與CPU的速度是相差很多的,所以為了保證并發(fā),在內(nèi)存與處理器之間引入了高速緩存(Cache),他的作用就是,在運算中,將使用到的數(shù)據(jù)拷貝到緩存中,當(dāng)運算結(jié)束后再從緩存同步到內(nèi)存之中,這樣就解決了IO與CPU之間的速度差異問題。
- 但是現(xiàn)在都是多核CPU,每個處理器都有自己的緩存,但是他們又共享同一主內(nèi)存,那么當(dāng)他們的運算涉及到同一塊主內(nèi)存時,就帶來了緩存不一致的問題,所以就有了緩存一致性協(xié)議,我以一張圖來表示他們之間的關(guān)系。
計算機硬件模型 -
java內(nèi)存區(qū)域
指的是java虛擬機在運行程序時管理的內(nèi)存,JVM把它劃分成幾個區(qū)域
Java虛擬機內(nèi)存區(qū)域- 方法區(qū)(Method Area):方法區(qū)屬于線程共享的內(nèi)存區(qū)域,又稱Non-Heap(非堆),主要用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),根據(jù)Java 虛擬機規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError 異常。值得注意的是在方法區(qū)中存在一個叫運行時常量池(Runtime Constant Pool)的區(qū)域,它主要用于存放編譯器生成的各種字面量和符號引用,這些內(nèi)容將在類加載后存放到運行時常量池中,以便后續(xù)使用。
- Java堆(Java Heap):Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機啟動時創(chuàng)建,是Java 虛擬機所管理的內(nèi)存中最大的一塊,主要用于存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做GC 堆,如果在堆中沒有內(nèi)存滿足實例分配需求,并且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。
- 程序計數(shù)器(Program Counter Register):屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。字節(jié)碼解釋器工作時,通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。
- 本地方法棧(Native Method Stacks):本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機用到的 Native 方法相關(guān)。
- 虛擬機棧(Java Virtual Machine Stacks):屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時創(chuàng)建,總數(shù)與線程關(guān)聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。每個方法執(zhí)行時都會創(chuàng)建一個棧楨來存儲方法的的變量表、操作數(shù)棧、動態(tài)鏈接方法、返回值、返回地址等信息。
-
java內(nèi)存模型
- 虛擬機規(guī)范中描述說Java內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則, 即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)。但是要注意的是,這里的變量不是通常意義上的變量,它包括了實例屬性、類屬性以及構(gòu)成數(shù)組的元素,但是不包括局部變量與方法參數(shù)。為什么這么說呢?
根據(jù)上面的Java內(nèi)存區(qū)域劃分我們可以知道,在虛擬機棧中有一張局部變量表,存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、 byte、 char、 short、 int、 float、 long、double) 、 對象引用(reference類型, 它不等同于對象本身, 可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔槪?也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置) 和returnAddress類型(指向了一條字節(jié)碼指令的地址),通俗一點說,如果局部變量是基本類型變量,則直接把這個變量的值保存在該變量對應(yīng)的內(nèi)存中.如果局部變量是引用類型的變量,則這個變量里存放的就是地址(注意的是:如果局部變量是一個reference類型, 它引用的對象在Java堆中可被各個線程共享, 但是reference本身在Java棧的局部變量表中, 它是線程私有的。),它們是線程私有的。
- 虛擬機規(guī)范中描述說Java內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則, 即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)。但是要注意的是,這里的變量不是通常意義上的變量,它包括了實例屬性、類屬性以及構(gòu)成數(shù)組的元素,但是不包括局部變量與方法參數(shù)。為什么這么說呢?
我給出一個例子,大家來看一下局部變量是否是線程安全的
public class Demo {
private int count = 0;
public static void main(String[] args) {
Demo d = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
d.safeAdd();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
d.unSafeAdd();
}
}).start();
}
}
public void unSafeAdd() {
for (int i = 0; i < 10000; i++) {
++count;
}
System.out.println("成員變量:" + count);
}
public void safeAdd() {
int count = 0;
for (int i = 0; i < 10000; i++) {
++count;
}
System.out.println("局部變量:" + count);
}
}
運行結(jié)果:
局部變量:10000
局部變量:10000
局部變量:10000
局部變量:10000
局部變量:10000
成員變量:10000
成員變量:20472
成員變量:29394
成員變量:40341
成員變量:43576
好了,了解完上面的,我們通過一張圖看一下JMM。

JMM.png
- 工作內(nèi)存
我們知道,jvm實際上是通過線程來執(zhí)行程序的,在每個線程創(chuàng)建的時候,jvm會給它分配一個工作內(nèi)存,從JMM來看,數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應(yīng)該包括程序計數(shù)器、虛擬機棧以及本地方法棧。從計算機硬件來看,工作內(nèi)存可能是Cache或者寄存器中。 - 主內(nèi)存
主要存儲的是Java實例對象,所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。(對比上面的局部變量來思考一下)在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應(yīng)該包括了堆和方法區(qū),從計算機硬件來看,主內(nèi)存也就是我們的硬件內(nèi)存。 - 工作方式
對于一個實例對象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型,將直接存儲在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會存儲在功能內(nèi)存的幀棧中,而對象實例將存儲在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。但對于實例對象的成員變量,不管它是基本數(shù)據(jù)類型還是引用類型,都會被存儲到堆區(qū)。至于static變量以及類本身相關(guān)信息將會存儲在主內(nèi)存中。需要注意的是,在主內(nèi)存中的實例對象可以被多線程共享,倘若兩個線程同時調(diào)用了同一個對象的同一個方法,那么兩條線程會將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才同步到主內(nèi)存。
最后我們來總結(jié)一下,JMM、java內(nèi)存區(qū)域以及計算機硬件之間的關(guān)系,
- JMM與java內(nèi)存區(qū)域:
JMM實際上描述的是一種規(guī)則,是邏輯上的劃分。JMM與Java內(nèi)存區(qū)域唯一相似點,都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應(yīng)該包括了堆和方法區(qū),數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應(yīng)該包括程序計數(shù)器、虛擬機棧以及本地方法棧。 -
JMM與計算機內(nèi)存
JVM中線程的操作最后都會映射到硬件上,在計算機硬件中,并沒有共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的劃分,實際上都是在計算機內(nèi)存中。工作內(nèi)存可能是Cache或者寄存器中,主內(nèi)存可能直接對應(yīng)于硬件中的內(nèi)存。**
JMM與計算機內(nèi)存


