為什麼需要程序、線程?
首先,我們來回憶一下馮諾依曼計算機體系。目前計算機主要是基于馮諾依曼體系結構設計的,下面就簡單分析一下馮諾依曼體系結構的計算機是如何工作的,首先下面的圖就是馮諾依曼體系結構圖。
馮諾依曼計算機體系結構,目前流程計算機組成如下:
引發的問題:如果單純依靠上述體系來做計算機處理,性能不高。主要瓶頸在IO上,比如磁盤IO、網絡IO等的讀取、寫入;而CPU性能非常高,往往是大部分時間是閑置的,就是沒有充分利用了CPU的性能。如下圖所示:
為了解決此問題,科學家、計算家們通過兩種方式優化了計算性能不高問題:1、增加記憶體、緩存;2、優化軟體結構
導入了記憶體、緩存(1級、2級、3級)、寄存器;需要資料各層進行複制
引入了程序、線程,讓線程搶占式的利用CPU資源;需要接管計算機硬體資源,任務排程、程序管理【線程】;在Java 層面即是synchronized、volatile、CAS、Lock、并發工具類、CountDownLatch、CyclicBarrier、Future、線程池等。
程式運作的底層原理
- 程式是什麼?QQ.exe,PowerPoint.exe
- 程序是什麼?程式啟動,進入記憶體,資源配置設定的基本單元
- 線程是什麼?程式執行的基本機關
- 程式如何開始運作? CPU 讀指令 - PC(存儲指令位址),讀資料到寄存器 Register,計算,回寫到記憶體;然後指向下一條指令
- 線程如何進行排程?Linux 線程排程器(OS)作業系統
- 線程切換的概念是什麼?Context Switch CPU 儲存現場執行新線程,恢複現場,繼續執行原線程這樣的一個過程
緩存、寄存器
從CPU的計算單元(ALU)到:
Registers | < 1 ns |
L1 cache | 約 1ns |
L2 cache | 約 3ns |
L3 cache | 約 15ns |
main memory | 約 80ns |
根據空間局部性原理,按塊讀取,程式局部性原理,可以提高效率;充分發揮總線、CPU針腳等一次性讀取更多資料的能力。
MESI Cache 緩存一緻性協定
緩存一緻協定是Modified、Exclusive、Shared、Invalid 四個單詞的縮寫,詳細知識參考:https://www.cnblogs.com/z00377750/p/9180644.html
Intel 的緩存一緻性協定叫MESI,其他處理器的緩存一緻性協定不叫MESI,叫:MSI、MOSI、Synapse Firefly Dragon
緩存行Cache Line:
- 緩存行越大,局部性空間效率越高,但讀取時間慢
- 緩存行越小,局部性空間效率越低,但讀取時間快
- 取一個折中值,目前多用:64位元組【工業實踐得出的結論】
根據緩存行一緻性協定,如果x的值被修改,在多線程環境下需要需要被通知到另外的其他線程。這個通知過程是需要花費時間的。
代碼示例1:資料在同一個緩存行,運作32728
/*
兩個線程操作在同一個緩存行的資料進行疊加計算,檢視時間耗時。緩存行大小64位元組,一個long類型8位元組
根據緩存一緻性協定,當同一緩存行資料更改了,必須通知其他線程,是以時間都花在了通知上面去了
*/
public class T01_CacheLinePadding_Deprecated {
public static long COUNT = 1_0000_10000L;
private static class T {
public volatile long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
}, "t1");
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start)/100_1000);
}
}
代碼示例2:資料不在同一個緩存行,運作16402,較示例1性能提升近1倍
/*
兩個線程操作在不在同一個緩存行的資料進行疊加計算,檢視時間耗時。緩存行大小64位元組,一個long類型8位元組
整體耗時較demo01 短,即快很多。
因為資料作了填充,保證兩個資料不在同一個緩存行中,是以不會觸發緩存一緻性問題,是以也就沒有需要互相通知的問題。
x,y肯定不在同一緩存行
JDK 1.7
*/
public class T02_CacheLinePadding_Deprecated {
public static long COUNT = 1_0000_0000;
// 定義p1~p7,確定x在同個緩存行
private static class T {
private long p1,p2,p3,p4,p5,p6,p7;
public volatile long x = 0L;
private long p9,p10,p11,p12,p13,p14,p15;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
}, "t2");
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start) / 100_000);
}
}
在項目中的應用有:disruptor、jdk中的
線程安全問題
因為CPU與外圍IO存在較大的性能差異,需要提高計算機的性能
- 硬體
- 加了記憶體、多級緩存,多核CPU緩存之間是不共享資料,引發了可見性問題
- 因為CPU 對線程任務的切換,引發了原子性問題
- CPU 要更加高效地去執行線程中的任務,引發了有序性的問題
- 軟體
- 增加了程序管理
- 增加了線程切換
- 增加了程序管理
線程安全問題 | 描述 | 解決 |
---|---|---|
1.原子性 | 表示不可分割 ---> 是因為多任務線程的切換 ---> 提高CPU使用率 ---> 提高并發性能 | synchronized,不僅僅能夠解決原子性不可分割,還保證可見性 |
2.可見性 | 其實本質上是CPU緩存 ---> 多核CPU緩存之間是不共享 ---> 可見性的問題 | volatile 保證多線程可見性,禁止CPU去緩存 |
3.有序性 | 解釋了CPU為何會把我們寫的Java代碼做一個重排序的操作 | volatile 禁止指令重排序 |
超線程:一個ALU對應多個PC Registers 所謂的四核八線程。注意:線程不是越多越好,線程切換需要時間。
鎖的概念
1、為什麼需要鎖?
CPU是亂序執行,CPU在進行讀等待的同時執行指令,是CPU亂序的根源;不是亂,而是提高效率。是以多線程環境下,對共享資料操作會引發産生資料線程安全的問題。
2、什麼是鎖?鎖解決什麼問題?
鎖是邏輯概念,保證資料一緻性。在同一個時刻,保證一個線程操作共享資料。在Java 開發中使用Synchronized 進行上鎖。
持有鎖的線程進行工作,那不持有鎖的線程咋辦?
- 忙等待;輕量級,自旋鎖,需要消耗CPU資源;适合競争不激烈
- 進隊列等待;重量級,需要作業系統進行排程;适合競争激烈
在JDK 早期,Synchronized 是直接向作業系統申請鎖,整個操作非常重;在JDK後期,對Synchronized 關鍵字鎖進行了更新,就是鎖有個更新的過程
Synchronized知識較多,特别花費了較多時間進行整理,下載下傳連結:Synchronized_思維導圖(全面).xmind.zip
案例一:多線程操作共享資料m進行++操作,最終導緻與理想結果不一緻問題
// 多線程,并發情況下對int m變量進行累加,出現的資料被寫覆寫。指派不是原子性的
public class T00_CasAndUnsafe {
private static int m = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
m++;
}
latch.countDown();
});
}
Arrays.stream(threads).forEach((t) -> t.start());
latch.await();
System.out.println(m);
}
}
解決方式一:為了解決資料共享問題,可以使用鎖對共享資料加鎖,如下示例:
// 多線程,并發情況下對 int m變量進行累加,資料回寫使用synchronized進行同步加鎖; 實作了資料的一緻性
public class T03_CasAndUnsafe3 {
private static /*volatile*/ int m = 0;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread[] threads = new Thread[10];
CountDownLatch latch = new CountDownLatch(threads.length);
Object o = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
synchronized (o) {
for (int j = 0; j < 10000; j++) {
m++;
}
latch.countDown();
}
});
}
Arrays.stream(threads).forEach((t) -> t.start());
latch.await();
System.out.println(m);
long end = System.currentTimeMillis();
long spend = end - start;
System.out.println("spend:" + spend + " ms");
}
}
解決方式二:為了解決資料共享問題,可以使用自旋鎖CAS對共享資料加鎖,如下示例:
// 多線程,并發情況下對 AtomicInteger m變量進行累加, 實作了資料的一緻性
public class T04_AtomicInteger {
private static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread[] threads = new Thread[10];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
m.getAndIncrement(); // m++;
}
latch.countDown();
});
}
Arrays.stream(threads).forEach((t) -> t.start());
latch.await();
System.out.println(m);
long end = System.currentTimeMillis();
long spend = end - start;
System.out.println("spend:" + spend + " ms");
}
}
CAS比較并交換,CAS原理圖如下:
CAS 引發的問題
文章最後,給大家推薦一些受歡迎的技術部落格連結:
- JAVA相關的深度技術部落格連結
- Flink 相關技術部落格連結
- Spark 核心技術連結
- 設計模式 —— 深度技術部落格連結
- 機器學習 —— 深度技術部落格連結
- Hadoop相關技術部落格連結
- 超全幹貨--Flink思維導圖,花了3周左右編寫、校對
- 深入JAVA 的JVM核心原了解決線上各種故障【附案例】
- 請談談你對volatile的了解?--最近小李子與面試官的一場“硬核較量”
- 聊聊RPC通信,經常被問到的一道面試題。源碼+筆記,包懂
- 深入聊聊Java 垃圾回收機制【附原理圖及調優方法】
歡迎掃描下方的二維碼或 搜尋 公衆号“大資料進階架構師”,我們會有更多、且及時的資料推送給您,歡迎多多交流!