From:深入理解Java虛擬機(jī)
- 目錄
BiBi - JVM -0- 開篇
BiBi - JVM -1- Java內(nèi)存區(qū)域
BiBi - JVM -2- 對象
BiBi - JVM -3- 垃圾收集算法
BiBi - JVM -4- HotSpot JVM
BiBi - JVM -5- 垃圾回收器
BiBi - JVM -6- 回收策略
BiBi - JVM -7- Java類文件結(jié)構(gòu)
BiBi - JVM -8- 類加載機(jī)制
BiBi - JVM -9- 類加載器
BiBi - JVM -10- 虛擬機(jī)字節(jié)碼
BiBi - JVM -11- 編譯期優(yōu)化
BiBi - JVM -12- 運(yùn)行期優(yōu)化
BiBi - JVM -13- 并發(fā)
基于【高速緩存】的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,同時也引入了一個問題:緩存一致性留瞳。在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存岳守,而它們又共享同一個主內(nèi)存,當(dāng)多個處理器的運(yùn)算任務(wù)涉及到同一塊主內(nèi)存區(qū)域時樊卓,將會導(dǎo)致數(shù)據(jù)緩存不一致密强。所以,要遵守緩存一致性協(xié)議患雏,在讀寫的時候要根據(jù)協(xié)議來進(jìn)行操作。
Java內(nèi)存模型 - JMM
Java虛擬機(jī)通過Java內(nèi)存模型【Java Memory Model罢维,JMM】來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異淹仑,以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果》畏酰【C/C++直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型】
JVM可以利用硬件的特性有:寄存器匀借、高速緩存、指令集中某些特有的指令平窘。
內(nèi)存間交互的8個操作:
lock
unlock
read
load
use
assign
store
它們都是原子的吓肋、不可再分的【對于long和double類型的變量JVM允許為非原子操作,但現(xiàn)在的商用JVM都選擇把64位數(shù)據(jù)的讀寫作為原子操作來對待瑰艘。所以是鬼,一般不需要把long和double專門聲明為volatile】。
把一個變量從主內(nèi)存復(fù)制到線程的工作內(nèi)存紫新,執(zhí)行read >> load均蜜;把一個變量從線程的工作內(nèi)存同步回主內(nèi)存,執(zhí)行store >> write芒率。
注意:Java內(nèi)存模型只要求上述兩個操作必須按照順序執(zhí)行兆龙,而沒有保證是連續(xù)執(zhí)行的。所以敲董,可能出現(xiàn)順序為:read a、read b慰安、load b腋寨、load a。
對一個變量執(zhí)行unlock操作之前化焕,必須先把此變量同步回主內(nèi)存中萄窜。
volatile
volatile變量在各個線程的工作內(nèi)存中不存在一致性問題,對所有線程都可見。但Java里面的運(yùn)算并非原子操作【如:++i】查刻,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣不是安全的键兜。
一條字節(jié)碼指令,不能代表執(zhí)行這條指令是一個原子操作穗泵。
volatile變量能夠禁止指令重排序優(yōu)化普气。
對一個volatile變量的寫操作【先行發(fā)生】于后面對這個變量的讀操作。
問題:線程A先調(diào)用setValue(7)佃延,然后線程B調(diào)用同一個對象的getVale()现诀,那么線程B會收到什么值?
private int value = 0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
答案:不確定履肃,0 或 7仔沿。如果改為private int volatile value = 0;
結(jié)果一定為7。
final的可見性:被final修飾的字段在構(gòu)造器中一旦初始化完成尺棋,并且構(gòu)造器沒有把this引用傳遞出去封锉,即沒有發(fā)生逃逸,那在其他線程中就能看見final字段膘螟。【因為該字段以后只能被讀成福,不能再被修改】【不可變對象一定是線程安全的,前提沒有this逃逸】
不可變對象萍鲸,最簡單的就是把對象中的成員都設(shè)為final闷叉,如:Integer中的private final int value;
。
線程
線程可以把一個進(jìn)程的資源分配和執(zhí)行調(diào)度分開脊阴,各個線程既可以共享進(jìn)程資源【如:內(nèi)存地址握侧、文件IO】,又可以獨立調(diào)度嘿期。
Java虛擬機(jī)的線程模型:JDK1.2之前是【用戶線程實現(xiàn)的】品擎,在JDK1.2之后,線程模型替換為【基于操作系統(tǒng)原生線程模型】來實現(xiàn)备徐。如:Sun JDK萄传,在Widows和Linux都是使用一對一的線程模型來實現(xiàn)的,即一條Java線程就映射到一條輕量級進(jìn)程中蜜猾,因為Widows和Linux系統(tǒng)提供的線程模型就是一對一的秀菱。
由于Java的線程是映射到操作系統(tǒng)原生線程上的,如果要阻塞或喚醒一個線程蹭睡,都需要操作系統(tǒng)來幫忙完成衍菱,這就需要從用戶狀態(tài)轉(zhuǎn)換到內(nèi)核狀態(tài),因此狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時間肩豁。
線程調(diào)度
協(xié)同式線程調(diào)度和搶占式線程調(diào)度脊串。
協(xié)調(diào)式:線程的執(zhí)行時間由線程本身來控制辫呻,線程把自己的工作執(zhí)行完后,要主動通知系統(tǒng)切換到另外一個線程上琼锋》殴耄【沒有線程同步問題】。缺點:當(dāng)一個線程編寫有錯誤缕坎,導(dǎo)致一直不告訴系統(tǒng)進(jìn)行線程切換怖侦,那就會一直阻塞。即一個進(jìn)程堅持不讓出CUP執(zhí)行時間念赶,使整個系統(tǒng)崩潰础钠。
搶占式【Java使用的線程調(diào)度方式】:每個線程由系統(tǒng)來分配執(zhí)行時間,線程的切換不由線程本身決定叉谜。此種情況旗吁,線程的執(zhí)行時間是系統(tǒng)可控的,不會出現(xiàn)讓一個線程導(dǎo)致阻塞整個進(jìn)程停局。
線程安全
對于線程安全的容器很钓,使用其方法時,也要注意同步問題董栽,如下:Vector是安全容器码倦,remove()和get()方法都是同步方法。
public void testSync() {
final Vector vector = new Vector(10);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); ++i) {
vector.remove(i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); ++i) {
vector.get(i);
}
}
}).start();
}
上面的例子锭碳,會產(chǎn)生數(shù)組越界問題袁稽,解決方案如下:
public void testSync() {
final Vector vector = new Vector(10);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); ++i) {
vector.remove(i);
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); ++i) {
vector.get(i);
}
}
}
}).start();
}
同步:互斥/阻塞同步、非阻塞同步
在JDK1.6之后擒抛,synchronized與ReentrantLock的性能基本上完全持平推汽,因此若基于性能因素而言,不再選擇ReentrantLock了歧沪,而是偏向于使用synchronized歹撒。
由于阻塞同步需要:加鎖、用戶狀態(tài)內(nèi)核轉(zhuǎn)換诊胞、維護(hù)鎖計數(shù)器暖夭、檢查是否有被阻塞的線程需要喚醒等,這些都會造成性能問題撵孤,所以這是一種悲觀的并發(fā)策略【原因:總是認(rèn)為只要不做同步處理迈着,那就肯定會出現(xiàn)問題】。
隨著硬件指令集的發(fā)展邪码,出現(xiàn)了【基于沖突檢測】的樂觀并發(fā)策略寥假,即非阻塞同步。
定義:先進(jìn)行操作霞扬,如果沒有其他線程爭用共享數(shù)據(jù),那操作就成功了;如果共享數(shù)據(jù)有爭用喻圃,產(chǎn)生了沖突萤彩,那就再采用其他的補(bǔ)償措施,如:不斷地重試斧拍,直到成功為止雀扶。該種方式不需要把線程掛起。
-
為什么非阻塞同步是隨著【硬件指令集的發(fā)展】才實現(xiàn)呢肆汹?
因為操作和沖突檢測這兩個步驟需要具備原子性愚墓,那靠什么來保證呢?如果這里再使用互斥同步來保證就失去意義了昂勉,所以只能依靠硬件來完成這件事情浪册,硬件保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成,如:比較并替換【Compare And Swap岗照,CAS】村象。
體會CAS的背景和應(yīng)用。
代碼體會:
public final int incrementAndGet() {
for (; ; ) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
}
}