JUC筆記(4)
16. JMM
請你談談你對 Volatile 的理解:
Volatile是java虛擬機器提供 輕量級的同步機制
1、保證可見性
2、不保證原子性
3、禁止指令重排序
什麼是JMM
JMM : Java記憶體模型,不存在的東西,概念!約定!
關於JMM的一些同步的約定:
1、執行緒解鎖前,必須把共享變數立刻重新整理回主存
2、執行緒加鎖前,必須讀取主存中的最新值到工作記憶體中
3、加鎖和解鎖是同一把鎖!
執行緒 工作記憶體 、主記憶體
記憶體互動操作有8種,虛擬機器實現必須保證每一個操作都是原子的,不可在分的(對於double和long類 型的變數來說,load、store、read和write操作在某些平臺上允許例外)
- lock (鎖定):作用於主記憶體的變數,把一個變數標識為執行緒獨佔狀態
- unlock (解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數 才可以被其他執行緒鎖定
- read (讀取):作用於主記憶體變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便 隨後的load動作使用
- load (載入):作用於工作記憶體的變數,它把read操作從主存中變數放入工作記憶體中
- use (使用):作用於工作記憶體中的變數,它把工作記憶體中的變數傳輸給執行引擎,每當虛擬機器 遇到一個需要使用到變數的值,就會使用到這個指令
- assign (賦值):作用於工作記憶體中的變數,它把一個從執行引擎中接受到的值放入工作記憶體的變 量副本中
- store (儲存):作用於主記憶體中的變數,它把一個從工作記憶體中一個變數的值傳送到主記憶體中, 以便後續的write使用
- write (寫入):作用於主記憶體中的變數,它把store操作從工作記憶體中得到的變數的值放入主內 存的變數中
JMM對這八種指令的使用,制定瞭如下規則:
- 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須 write
- 不允許執行緒丟棄他近的assign操作,即工作變數的資料改變了之後,必須告知主存
- 不允許一個執行緒將沒有assign的資料從工作記憶體同步回主記憶體
- 一個新的變數必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變數。就是懟變數 實施use、store操作之前,必須經過assign和load操作
- 一個變數同一時間只有一個執行緒能對其進行lock。多次lock後,必須執行相同次數的unlock才能解 鎖 如果對一個變數進行lock操作,會清空所有工作記憶體中此變數的值,在執行引擎使用這個變數前, 必須重新load或assign操作初始化變數的值
- 如果一個變數沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他執行緒鎖住的變數
- 對一個變數進行unlock操作之前,必須把此變數同步回主記憶體
問題: 程式不知道主記憶體的值已經被修改過了
17. Volatile
(1) 保證可見性
public class JMMDemo {
// 不加 volatile 程式就會死迴圈!
// 加 volatile 可以保證可見性
private volatile static int num = 0;
public static void main(String[] args) { // main
new Thread(()->{ // 執行緒 1 對主記憶體的變化不知道的
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
num的修改被thread可見了
(2) 不保證原子性
原子性 : 不可分割
執行緒A在執行任務的時候,不能被打擾的,也不能被分割。要麼同時成功,要麼同時失敗。
// volatile 不保證原子性
public class VDemo02 {
// volatile 不保證原子性
// 原子類的 Integer
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
// num++; // 不是一個原子性操作
num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
}
public static void main(String[] args) {
//理論上num結果應該為 2 萬
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
但是如果不加 lock 和 synchronized ,怎麼樣保證原子性?
num++其實不是個原子性操作。
因此使用原子類,解決原子性問題,沒必要lock:
這些類的底層都直接和作業系統掛鉤!在記憶體中修改值!Unsafe類是一個很特殊的存在!
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
(3) 指令重排
什麼是 指令重排:你寫的程式,計算機並不是按照你寫的那樣去執行的。
原始碼-->編譯器優化的重排--> 指令並行也可能會重排--> 記憶體系統也會重排---> 執行
處理器在進行指令重排的時候,考慮:資料之間的依賴性!
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我們所期望的:1234 但是可能執行的時候回變成 2134 1324 可不可能是 4123!
可能造成影響的結果: a b x y 這四個值預設都是 0;
volatile可以避免指令重排: 記憶體屏障。CPU指令。
作用:
1、保證特定的操作的執行順序!
2、可以保證某些變數的記憶體可見性 (利用這些特性volatile實現了可見性)
Volatile 是可以保持 可見性。不能保證原子性,由於記憶體屏障,可以保證避免指令重排的現象產生!
18. 徹底玩轉單例模式
記憶體屏障在單例模式使用最多
餓漢式
// 餓漢式單例
public class Hungry {
// 可能會浪費空間
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
DCL 懶漢式(鎖):
問題:為什麼加volatile?因為volatile可以避免指令重排
// 懶漢式單例
// 道高一尺,魔高一丈!
/**
lazyMan = new LazyMan();
* 1. 分配記憶體空間
* 2、執行構造方法,初始化物件
* 3、把這個物件指向這個空間
*
* 123
* 132 A
* B // 此時lazyMan還沒有完成構造
*/
public class LazyMan {
private static boolean flag = false;
private LazyMan(){
synchronized (LazyMan.class){
if (flag == false){
flag = true;
}else {
throw new RuntimeException("不要試圖使用反射破壞異常");
}
}
}
private volatile static LazyMan lazyMan;
// 雙重檢測鎖模式的 懶漢式單例 DCL懶漢式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan(); // 不是一個原子性操作
}
}
}
return lazyMan;
}
// 反射!
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Field qinjiang = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//獲得空參構造器
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
flag.set(instance,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
這樣雖然解決了問題,但是因為用到了synchronized
,會導致很大的效能開銷,並且加鎖其實只需要在第一次初始化的時候用到,之後的呼叫都沒必要再進行加鎖。
雙重檢查鎖(double checked locking)是對上述問題的一種優化。先判斷物件是否已經被初始化,再決定要不要加鎖。
此外,反射可以破壞這種單例,因此可以在無參構造裡丟擲異常。
靜態內部類
// 靜態內部類
public class Holder {
private Holder(){
}
public static Holder getInstace(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
列舉
// enum 是一個什麼? 本身也是一個Class類
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
// NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
System.out.println(instance1);
System.out.println(instance2);
}
}
反射不能破壞列舉的單例模式。java.lang.enum 可以看到列舉沒有無參構造器。
用有參構造器:
19. 深入理解CAS
什麼是CAS: compare and set 比較並交換
大廠你必須要深入研究底層!有所突破! 修內功,作業系統,計算機網路
valueOffset:記憶體地址偏移值
Unsafe類:
var1+var2=var5,則var5=var5+1
自旋鎖:不停旋轉,直到這個值能夠成功為止
CAS:
比較當前工作記憶體中的值和主記憶體中的值,如果這個值是期望的,那麼執行操作!如果不是就一直迴圈!
好處:自帶原子性
缺點:
- 迴圈會消耗CPU資源
- 一次性只能保證一個共享變數的原子性
- ABA問題
ABA問題:(狸貓換太子)
public class CASDemo {
// CAS compareAndSet : 比較並交換!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新 public final boolean compareAndSet(int expect, int update);
// 如果我期望的值達到了,那麼就更新,否則,就不更新, CAS 是CPU的併發原語! // ============== 搗亂的執行緒 ================== System.out.println(atomicInteger.compareAndSet(2020, 2021)); System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// ============== 期望的執行緒 =================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
這就是樂觀鎖,只要判斷鎖沒被動過,就修改值。
樂觀鎖假設資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則返回給使用者錯誤的資訊,讓使用者決定如何去做。樂觀鎖適用於讀操作多的場景,這樣可以提高程式的吞吐量。
樂觀鎖是相對悲觀鎖而言,也是為了避免資料庫幻讀、業務處理時間過長等原因引起資料處理錯誤的一種機制,但樂觀鎖不會刻意使用資料庫本身的鎖機制,而是依據資料本身來保證資料的正確性。樂觀鎖的實現:
- CAS 實現:Java 中java.util.concurrent.atomic包下面的原子變數使用了樂觀鎖的一種 CAS 實現方式。
- 版本號控制:一般是在資料表中加上一個資料版本號 version 欄位,表示資料被修改的次數。當資料被修改時,version 值會+1。當執行緒A要更新資料值時,在讀取資料的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值與當前資料庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功。
悲觀鎖分為共享鎖和排他鎖。
https://www.jianshu.com/p/d2ac26ca6525
20. 原子引用AtomicReference
解決ABA 問題,引入原子引用! 對應的思想:樂觀鎖!
帶版本號的原子操作!
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一個包裝類,注意物件的引用問題
// 正常在業務操作,這裡面比較的都是一個個物件;Integer不能超過-128~127
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
// CAS compareAndSet : 比較並交換!
public static void main(String[] args) {
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 獲得版本號
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Lock lock = new ReentrantLock(true);
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("a2=>"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));//版本號+1
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
// 樂觀鎖的原理相同!
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 獲得版本號
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
}
Integer 使用了物件快取機制,預設範圍是 -128 ~ 127 ,推薦使用靜態工廠方法 valueOf 獲取物件例項,而不是 new,因為 valueOf 使用快取,而 new 一定會建立新的物件分配新的記憶體空間;
21. 各種鎖的理解
(1) 公平鎖、非公平鎖
公平鎖: 非常公平, 不能夠插隊,必須先來後到!
非公平鎖:非常不公平,可以插隊 (預設都是非公平)效率高
public ReentrantLock(){
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(2) 可重入鎖(遞迴鎖)
所有鎖都是可重入鎖。
// Synchronized
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName() + "sms");
call(); // 這裡也有鎖
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + "call");
}
}
可以看到儘管呼叫了其他方法,可以理解為同一把鎖。
Lock鎖:
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock(); // 細節問題:lock.lock(); lock.unlock(); // lock 鎖必須配對,否則就會死在裡面
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "sms");
call(); // 這裡也有鎖
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
(3) 自旋鎖(spinlock)
不斷嘗試,直到成功
我們來自定義一個鎖測試
/**
* 自旋鎖
*/
public class SpinlockDemo {
// int 0
// Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加鎖
public void myLock(){
Thread thread = Thread.currentThread();
// 自旋鎖
while (!atomicReference.compareAndSet(null,thread)){
//如果執行緒是空的,就把它丟進去無限迴圈
}
System.out.println(Thread.currentThread().getName() + "==> mylock");
}
// 解鎖
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
測試
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
// 底層使用的自旋鎖CAS
SpinlockDemo lock = new SpinlockDemo();
new Thread(()-> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()-> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T2").start();
}
}
t1先拿到鎖,t2卡在自旋前面,然後t1解鎖,t2才有資格拿到鎖並解鎖。
(4) 死鎖
死鎖是什麼: 兩個人互相搶奪資源
產生死鎖的原因主要是:
(1) 因為系統資源不足。
(2) 程式執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程式的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程式執行推進順序與速度不同,也可能產生死鎖。
產生死鎖的四個必要條件:
死鎖的四要素:互斥使用資源,佔用等待資源,不可搶佔資源,迴圈等待資源
(1) 互斥條件:一個資源每次只能被一個程式使用。
(2) 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程式已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
死鎖測試,怎麼排除死鎖
關於synchronized () 括號中應該傳什麼物件?https://blog.csdn.net/qq_35993946/article/details/86359250
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";//兩個鎖的物件
new Thread(new MyThread(lockA, lockB), "T1").start();//A想拿B
new Thread(new MyThread(lockB, lockA), "T2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "lock:"+lockA+"=>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "lock:"+lockB+"=>get"+lockA);
}
}
}
}
解決問題:
1)使用 jps -l 定位程式號
獲得當前活著的一些程式
2)使用 jstack 程式號 找到死鎖問題:
jstack 11444
談談面試:工作中!如何排查問題!
- 日誌
- 堆疊 (區分度)
相關文章
- Ty-JUC基礎筆記筆記
- JUC執行緒筆記(一)執行緒筆記
- 學習筆記 07 --- JUC集合筆記
- JUC整理筆記一之細說Unsafe筆記
- JUC原始碼學習筆記6——ReentrantReadWriteLock原始碼筆記
- docker 筆記4Docker筆記
- JUC原始碼學習筆記2——AQS共享和Semaphore,CountDownLatch原始碼筆記AQSCountDownLatch
- JAVA自學筆記(4)Java筆記
- c++筆記4C++筆記
- 4,子程式(筆記)筆記
- 課堂筆記4筆記
- 閱讀筆記4筆記
- JUC 常用4大併發工具類
- JUC併發程式設計學習筆記(四)8鎖現象程式設計筆記
- JUC併發程式設計學習筆記(六)Callable(簡單)程式設計筆記
- 第4關-精華筆記筆記
- swift學習筆記《4》Swift筆記
- Day4晚筆記筆記
- FPGA讀書筆記4FPGA筆記
- Rails 4 學習筆記AI筆記
- CCNA學習筆記4筆記
- Delphi逆向工程筆記[4]筆記
- vue學習筆記4Vue筆記
- Java學習筆記4Java筆記
- JUC併發程式設計學習筆記(七)常用的輔助類程式設計筆記
- python學習筆記4Python筆記
- Webpack4學習筆記Web筆記
- p4 學習筆記筆記
- webpack4實操筆記Web筆記
- webpack4 + typescript 配置筆記WebTypeScript筆記
- QT學習筆記4(動畫)QT筆記動畫
- Android學習筆記(4)Android筆記
- 【Go學習筆記4】切片Go筆記
- STREAMS筆記(4) 排表 & 加表筆記
- 學車筆記(第4天)筆記
- JAVA4ANDROID筆記JavaAndroid筆記
- PL/SQL學習筆記-4SQL筆記
- ruby字串學習筆記4字串筆記