JAVA 併發與高併發知識筆記(三)
一、原子性操作的幾種方式
先回顧一下原子性操作的解釋:原子性要有互斥性,既:同一時刻只能有一個執行緒進行操作。
1、synchronized 關鍵字(同步鎖),由JVM 管理以及實現
a) 在這個關鍵字作用物件的物件範圍內,多個執行緒操作是原子性的。(注意:是作用物件的作用範圍內)
b) 關鍵字常見使用方式
b.1 修飾程式碼塊,作用於呼叫物件,被修飾的程式碼塊同一個物件同一時刻只能有一個執行緒去訪問
b.2 修飾方法,作用於呼叫物件,被修飾的方法同一個物件同一時刻只能有一個執行緒去訪問
b.3 修飾靜態方法,作用於所有物件,被修飾的方法在該類任何例項物件呼叫時都只能有一個執行緒訪問
b.4 修飾類(引數為class),作用於所有物件,類大括號括起來的,相當於每個方法都加了關鍵字
c) 示例
@Slf4j public class SynchronizedTest { /** * 未做任何修飾 */ public void test(int n) { for (int i = 0; i < 5; i++) { log.info("test->{}-{}", n, i); } } /** * 修飾程式碼塊 */ public void test1(int n) { log.info("程式碼塊A-{}", n); synchronized (this) { for (int i = 0; i < 5; i++) { log.info("test1->{}-{}", n, i); } } log.info("程式碼塊B-{}", n); } /** * 修飾方法 */ public synchronized void test2(int n) { log.info("方法A-{}", n); for (int i = 0; i < 5; i++) { log.info("test2->{}-{}", n, i); } log.info("方法B-{}", n); } /** * 修飾靜態方法 */ public synchronized static void test3(int n) { log.info("靜態方法A-{}", n); for (int i = 0; i < 5; i++) { log.info("test3->{}-{}", n, i); } log.info("靜態方法B-{}", n); } /** * 鎖類 */ public void test4(int n) { synchronized (this.getClass()) { log.info("類A-{}", n); for (int i = 0; i < 5; i++) { log.info("test4->{}-{}", n, i); } log.info("類B-{}", n); } } public static void main(String[] args) { /* 每個需要單獨執行 */ oneTest(); // twoTest(); // threeTest(); // fourTest(); // allTest1(); // allTest2(); } // 測試修飾程式碼塊 public static void oneTest() { // 先輸執行非同步塊,但是不包含同步塊之後的程式碼,然後執行同步塊程式碼,最後執行同步塊之後的程式碼 final SynchronizedTest st = new SynchronizedTest(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { st.test1(1); } }); es.execute(new Runnable() { @Override public void run() { st.test1(2); } }); es.shutdown(); } // 測試修飾方法 public static void twoTest() { // 在我本機測試結果是,先執行完 21,在執行22 final SynchronizedTest st = new SynchronizedTest(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { st.test2(21); } }); es.execute(new Runnable() { @Override public void run() { st.test2(22); } }); es.shutdown(); } // 測試修飾靜態方法 public static void threeTest() { // 在我本機測試結果是,先執行完31,後執行32 ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { SynchronizedTest.test3(31); } }); es.execute(new Runnable() { @Override public void run() { SynchronizedTest.test3(32); } }); es.shutdown(); } // 測試鎖class public static void fourTest() { // 在我本機結果是,順序執行,先執行完 41 ,然後執行 42 final SynchronizedTest st = new SynchronizedTest(); final SynchronizedTest st2 = new SynchronizedTest(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { st.test4(41); } }); es.execute(new Runnable() { @Override public void run() { st2.test4(42); } }); es.shutdown(); } // 綜合測試,測試靜態方法與非靜態方法同時呼叫 public static void allTest1() { // 在我本機測試結果是,靜態方法總是先執行,然後是非靜態方法 final SynchronizedTest st = new SynchronizedTest(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { st.test1(51); } }); es.execute(new Runnable() { @Override public void run() { SynchronizedTest.test3(52); } }); es.shutdown(); } // 綜合測試,測試非加鎖方法與加鎖方法 public static void allTest2() { // 在我本機結果是亂序執行 final SynchronizedTest st = new SynchronizedTest(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Runnable() { @Override public void run() { st.test(61); } }); es.execute(new Runnable() { @Override public void run() { st.test1(62); } }); es.shutdown(); } }
d) synchronized 關鍵字不會被子類繼承
e) synchronized 不可中斷,線上程競爭激烈的時候會降低效能
2、lock 鎖,這個是 JDK 提供的程式碼層面的鎖,依賴於CPU特殊指令,典型的類 ReentrantLock (可重入鎖,synchronized 也是可重入鎖,既同一個執行緒持有鎖後可以再次進行加鎖)
a) lock 屬於程式碼級別的鎖,它可以被中斷,在競爭激烈的時候效能依然保持常態,但是需要注意加鎖後必須有相應的解鎖操作,否則很容易造成死鎖導致系統性能下降或者癱瘓
3、atomic 包效能比JUC中的lock 效能高,但是一次只能處理一個值
二、執行緒安全可見性
1、解釋:某個執行緒對主記憶體共享變數的修改能夠及時被其它執行緒觀察到
2、導致共享變數的修改沒有及時被觀察到的原因
a) 執行緒交叉執行
b) 重排序與交叉執行同時存在
c) 共享變數的值沒有線上程工作記憶體與主記憶體之間及時更新
3、JVM 對 synchronized 規定
a) 執行緒解鎖前必須將工作記憶體中的最新值重新整理到主記憶體中
b) 執行緒加鎖時將清空工作記憶體中共享變數的值,執行緒需要從主記憶體中獲取最新的值,加鎖與解鎖必須是同一個把鎖
4、volatile 記憶體可見性
volatile 核心是使用記憶體屏障(指令)以及禁止指令重排序來保證可見性
a) 對 volatile 寫操作時,會在寫操作後加入 store 指令將工作記憶體值重新整理到主記憶體,從而保證可見性,如下解釋
普通讀 -> 普通寫 -> 屏障(store-store) -> volatile 寫-> 屏障(store-load) ,第一個屏障防止與前面的寫進行排序,第二個屏障用於防止之後可能出現的讀寫進行排序
c) 對 voltaile 讀讀操作時,會在讀操作前加入 load 指令,將主記憶體最新值載入到工作記憶體
volatile 讀 -> 屏障(load-load)->屏障(load-store) -> 普通讀->普通寫 ,第一個屏障防止之後的所有讀操作發生指令重排序,第二個屏障是防止之後的寫操作不與之前的讀操作發生指令重排序。
5、指令重排序規則( happens - before 原則)
- 程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
- 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;
- volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作;
- 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
- 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作;
- 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;
- 執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;
- 物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始;
如果不符合以上規則或者不能從以上規則進行推匯出來,則會發生指令重排序。
三、物件釋出
a) 釋出物件:使一個物件能夠被當前範圍之外的程式碼使用
示例
/**
* 物件釋出
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO1 {
public ReleasePOJO1() {
}
private String[] array = { "1", "2", "3" };
// 不安全
// 這種情況下如果多個執行緒或者單執行緒中對返回的陣列引用進行了修改,則會修改原始值
public String[] getArray() {
return array;
}
// 安全
// 以副本方式返回資料,及時發生修改,那麼改的只是副本
public String[] getArray2() {
if (array == null) {
return array;
}
return Arrays.copyOf(array, array.length);
}
public static void main(String[] args) {
ReleasePOJO1 rp = new ReleasePOJO1();
// 不安全釋出
// 原值
log.info("修改前array1-{}", Arrays.toString(rp.getArray()));
// 修改值
rp.getArray()[0] = "A";
log.info("修改後array1-{}", Arrays.toString(rp.getArray()));
// 安全釋出
log.info("修改前array2-{}", Arrays.toString(rp.getArray2()));
// 修改值
rp.getArray2()[0] = "B";
log.info("修改後array2-{}", Arrays.toString(rp.getArray2()));
}
}
b) 物件逸出:不安全的物件釋出,當一個物件還沒有構造完成時就被其它執行緒使用,這種寫法可能會引發未知問題
示例
/**
* 物件逸出演示(錯誤示例程式碼,不建議程式設計時這樣寫程式碼)
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO2 {
private String t;
public ReleasePOJO2() {
log.info("POJO2 - 1 - {}", Thread.currentThread().getName());
new Thread(new POJO()).start();
t = "測試逸出";
log.info("POJO2 - 2 - {}", Thread.currentThread().getName());
}
private class POJO implements Runnable {
@Override
public void run() {
log.info("Thread - {}", Thread.currentThread().getName());
// 由於 this 物件 尚未初始化完,所以會先去初始化 this 物件,然後才會執行
log.info(ReleasePOJO2.this.t);
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
es.execute(new Runnable() {
@Override
public void run() {
new ReleasePOJO2();
}
});
}
es.shutdown();
}
}
四、安全物件釋出的幾種方式
1、在靜態初始化函式中初始化物件的引用
2、將物件引用儲存在volatile 或 AtomicReference 物件中
3、將物件引用儲存在正確構造的某個物件的一個 final 域中
4、將物件引用儲存在由鎖保護的域中(如示例一)
示例一
/**
* 靜態釋出示例
*
* @author Aaron
*
*/
public class ReleasePOJO3 {
private volatile static ReleasePOJO3 p3;
private ReleasePOJO3() {
}
public static ReleasePOJO3 getInstance() {
if (p3 == null) {
synchronized (ReleasePOJO3.class) {
if (p3 == null) {
p3 = new ReleasePOJO3();
}
}
}
return p3;
}
}
示例二
/**
* 列舉方式釋出
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO5 {
private ReleasePOJO5() {
}
// 實際使用時建議單獨放在一個類檔案中
public enum Instance {
P5;
// 這裡由JVM來保證只會被初始化一次
Instance() {
p5 = new ReleasePOJO5();
}
private ReleasePOJO5 p5;
public ReleasePOJO5 getP5() {
return p5;
}
}
public static void main(String[] args) {
log.info("{}", Instance.P5.hashCode());
log.info("{}", Instance.P5.hashCode());
}
}
五、靜態程式碼塊與靜態函式執行順序問題
1、靜態程式碼是按照順序載入
// 該程式碼輸出結果是null
@Slf4j
public class ReleasePOJO4 {
static {
// 初始化
p4 = new ReleasePOJO4();
}
// 賦值為 null
private static ReleasePOJO4 p4 = null;
public static ReleasePOJO4 getInstance() {
return p4;
}
public static void main(String[] args) {
// 輸出是null
log.info("{}", ReleasePOJO4.getInstance());
}
}
// 該程式碼輸出正常
@Slf4j
public class ReleasePOJO4 {
// 賦值為 null
private static ReleasePOJO4 p4 = null;
static {
// 初始化
p4 = new ReleasePOJO4();
}
public static ReleasePOJO4 getInstance() {
return p4;
}
public static void main(String[] args) {
// 輸出是 正常的
log.info("{}", ReleasePOJO4.getInstance());
}
}
六、不可變物件
1、建立後就不能修改內部狀態的類
2、物件所有域都是final的以及類被宣告為final的,如 String 類
3、物件建立期間沒有逸出
七、可變物件轉不可變物件工具包
1、標準 JAVA 庫中的
2、第三方庫(Guava,這裡以List 為例,它實現了List 介面,並對 addAll 等會對內容進行變更的操作進行了異常丟擲)
七、執行緒封閉
執行緒封閉基本理解:操作的所有變數、物件狀態都在該執行緒內完成,其它執行緒也不會影響到該執行緒。
1、實現執行緒封閉的幾種方式
a) Ad-hoc 方式實現執行緒封閉,這種方式最不推薦,因為是由程式邏輯控制,這樣的實現很糟糕,並且很難維護
b) 堆疊封閉:區域性變數,主要是方法內變數,執行緒執行的時候都操作的是自己的本地記憶體,因此併發是安全的
c) ThreadLocal 實現執行緒封閉,主要原理是 ThreadLocal 為每一個執行緒儲存一個變數副本,主要使用map來儲存變數值,比較好的執行緒封閉的實現,使用時需要注意記憶體洩露問題,線上程結束時呼叫remove 來清除副本,還有一點,如果傳入的是物件或陣列,那麼在子執行緒操作該物件或陣列時會改變原始值,所以用於讀沒問題,但是如果多個執行緒去寫存放的物件值,那麼這個共享物件本身必須是執行緒安全的才行,如程式碼示例。
@Slf4j
public class ThreadlocalTest {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
final ThreadLocal<ThreadlocalTest> tl = new ThreadLocal<>();
// 原始物件
final ThreadlocalTest tt = new ThreadlocalTest();
tt.setName("哈哈哈");
new Thread(new Runnable() {
@Override
public void run() {
tl.set(tt);
tl.get().setName("哈哈哈哈1");
log.info(tt.getName());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
tl.set(tt);
tl.get().setName("哈哈哈哈2");
log.info(tt.getName());
}
}).start();
log.info("main - {}", tt.getName());
}
}