日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

【Java】多线程SynchronizedVolatile、锁升级过程 - 预习+第一天笔记

發布時間:2024/2/28 java 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【Java】多线程SynchronizedVolatile、锁升级过程 - 预习+第一天笔记 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

預習

1.什么是線程

基本概念

我們先從線程的基本概念開始,給大家復習一下,不知道有多少同學是基礎不太好,說什么是線程都不知道的,如果這樣的話,花時間去補初級內容的課。

什么是叫一個進程? 什么叫一個線程?

  • Program app ->QQ.exe

進程:做一個簡單的解釋,你的硬盤上有一個簡單的程序,這個程序叫QQ.exe,這是一個程序,這個程序是一個靜態的概念,它被扔在硬盤上也沒人理他,但是當你雙擊它,彈出一個界面輸入賬號密碼登錄進去了,OK,這個時候叫做一個進程。進程相對于程序來說它是一個動態的概念

線程:作為一個進程里面最小的執行單元它就叫一個線程,用簡單的話講一個程序里不同的執行路徑就叫做一個線程

示例:什么叫做線程

package com.mashibing.juc.c_000;import java.util.concurrent.TimeUnit;public class T01_WhatIsThread {private static class T1 extends Thread {@Overridepublic void run() {for(int i=0; i<10; i++) {try {TimeUnit.MICROSECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("T1");}}}public static void main(String[] args) {//new T1().run();new T1().start();for(int i=0; i<10; i++) {try {TimeUnit.MICROSECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main");}} }

觀察上面程序的數據結果,你會看到字符串“T1”和“Main”的交替輸出,這就是程序中有兩條不同的執行路徑在交叉執行,這就是直觀概念上的線程,概念性的東西,理解就好,沒有必要咬文嚼字的去背文字的定義。

2.線程常用方法

我們來認識幾個線程的方法

package com.mashibing.juc.c_000;public class T03_Sleep_Yield_Join {public static void main(String[] args) {//testSleep();//testYield();testJoin();} /*Sleep,意思就是睡眠,當前線程暫停一段時間讓給別的線程去運行。Sleep是怎么復活的?由你的睡眠時間而定,等睡眠到規定的時間自動復活*/static void testSleep() {new Thread(()->{for(int i=0; i<100; i++) {System.out.println("A" + i);try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500)} catch (InterruptedException e) {e.printStackTrace();}}}).start();}/*Yield,就是當前線程正在執行的時候停止下來進入等待隊列,回到等待隊列里在系統的調度算法里頭呢還是依然有可能把你剛回去的這個線程拿回來繼續執行,當然,更大的可能性是把原來等待的那些拿出一個來執行,所以yield的意思是我讓出一下CPU,后面你們能不能搶到那我不管*/static void testYield() {new Thread(()->{for(int i=0; i<100; i++) {System.out.println("A" + i);if(i%10 == 0) Thread.yield();}}).start();new Thread(()->{for(int i=0; i<100; i++) {System.out.println("------------B" + i);if(i%10 == 0) Thread.yield();}}).start();}/*join, 意思就是在自己當前線程加入你調用Join的線程(),本線程等待。等調用的線程運行完了,自己再去執行。t1和t2兩個線程,在t1的某個點上調用了t2.join,它會跑到t2去運行,t1等待t2運行完畢繼續t1運行(自己join自己沒有意義) */static void testJoin() {Thread t1 = new Thread(()->{for(int i=0; i<100; i++) {System.out.println("A" + i);try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500)} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(()->{try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}for(int i=0; i<100; i++) {System.out.println("A" + i);try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500)} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();} }

線程狀態轉移圖

3.啟動線程的五種方式

創建線程的幾種方式

package com.mashibing.juc.c_000;import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask;public class T02_HowToCreateThread {static class MyThread extends Thread {@Overridepublic void run() {System.out.println("Hello MyThread!");}}static class MyRun implements Runnable {@Overridepublic void run() {System.out.println("Hello MyRun!");}}static class MyCall implements Callable<String> {@Overridepublic String call() {System.out.println("Hello MyCall");return "success";}}//啟動線程的5種方式public static void main(String[] args) {new MyThread().start();new Thread(new MyRun()).start();new Thread(()->{System.out.println("Hello Lambda!");}).start();Thread t = new Thread(new FutureTask<String>(new MyCall()));t.start();ExecutorService service = Executors.newCachedThreadPool();service.execute(()->{System.out.println("Hello ThreadPool");});service.shutdown();}}

分享一道面試題

請你告訴我啟動線程的三種方式 ?
你說第一個:new Thread().start(); 第二個: new Thread(Runnable).start() 這沒問題 ;那第三個呢,要回答線程池也是用的這兩種之一,他這么問有些吹毛求疵的意思,你就可以說通過線程池也可以啟動一個新的線程 3:Executors.newCachedThreadPool()或者FutureTask + Callable

4.線程同步的基本概念

synchronized

下面我們來講synchronized關鍵字,有不少同學已經耳熟能詳了,不過作為復習還是要復習一下。第一個是多個線程去訪問同一個資源的時候對這個資源上鎖。

為什么要上鎖呢?訪問某一段代碼或者某臨界資源的時候是需要有一把鎖的概念在這兒的。

比如:我們對一個數字做遞增,兩個程序對它一塊兒來做遞增,遞增就是把一個程序往上加1啊,如果兩個線程共同訪問的時候,第一個線程一讀它是0,然后把它加1,在自己線程內部內存里面算還沒有寫回去的時候而第二個線程讀到了它還是0,加1在寫回去,本來加了兩次,但還是1,那么我們在對這個數字遞增的過程當中就上把鎖,就是說第一個線程對這個數字訪問的時候是獨占的,不允許別的線程來訪問,不允許別的線程來對它計算,我必須加完1收釋放鎖,其他線程才能對它繼續加。

實質上,這把鎖并不是對數字進行鎖定的, 你可以任意指定,想鎖誰就鎖誰。

我第一個小程序是這么寫的 ,如果說你想上了把鎖之后才能對count進行減減訪問,你可以new一個Object,所以這里鎖定就是o,當我拿到這把鎖的時候才能執行這段代碼。是鎖定的某一個對象,synchronized有一個鎖升級的概念,我們一會兒會講到

/** *synchronized關鍵字 *對某個對象加鎖 *@author mashibing */ package com.mashibing.juc.c_001;public class T {private int count = 10;private Object o = new Object();public void m() {synchronized(o) { //任何線程要想執行下面的代碼,必須先拿到o的鎖count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}}}

?

我們來談一下synchronized它的一些特性。如果說你每次都定義個一個鎖的對象Object o 把它new出來那加鎖的時候太麻煩每次都要new一個新的對象出來,所以呢,有一個簡單的方式就是**synchronized(this)**鎖定當前對象就行

/*** synchronized關鍵字* 對某個對象加鎖* @author mashibing*/ package com.mashibing.juc.c_002;public class T {private int count = 10;public void m() {synchronized(this) { ?//任何線程想要執行那個下面的代碼,必須先要拿到this的鎖count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}}}

如果你要是鎖定當前對象呢,你也可以寫成如下方法。synchronized方法和synchronized(this)執行這段代碼它是等值的

package com.mashibing.juc.c_003;public class T {private int count = 10;public synchronized void m() { //等同于在方法的代碼執行時要synchronized(this)count--;System.out.println(Thread.currentThread().getName() + " count = " + count);} }

我們知道靜態方法static是沒有this對象的,你不需要new出一個對象來就能執行這個方法,但如果這個這個上面加一個synchronized的話就代表synchronized(T.class)。這里這個synchronized(T.class)鎖的就是T類的對象

package com.mashibing.juc.c_004;public class T {private static int count = 10;public synchronized static void m() { //這里等同于synchronized(T.class)count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void mm() {synchronized(T.class) { //考慮一下這里寫synchronized(this)是否可以?count --;}} }

問題:T.class是單例的嗎?

一個class load到內存它是不是單例的,想想看。一般情況下是,如果是在同一個ClassLoader空間那它一定是。不是同一個類加載器就不是了,不同的類加載器互相之間也不能訪問。所以說你能訪問它,那他一定就是單例

下面程序:很有可能讀不到別的線程修改過的內容,除了這點之外count減減完了之后下面的count輸出和你減完的結果不對,很容易分析:如果有一個線程把它從10減到9了,然后又有一個線程在前面一個線程還沒有輸出呢進來了把9又減到了8,繼續輸出的8,而不是9。如果你想修正它,前面第一個是在上面加volatile,改了馬上就能得到。

/*** 分析一下這個程序的輸出* @author mashibing*/ package com.mashibing.juc.c_005;public class T implements Runnable {private /*volatile*/ int count = 100;public /*synchronized*/ void run() { count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void main(String[] args) {T t = new T();for(int i=0; i<100; i++) {new Thread(t, "THREAD" + i).start();}}}

另外這個之外還可以加synchronized,加了synchronized就沒有必要在加volatile了,因為synchronized既保證了原子性,又保證了可見性。

//對比上一個小程序 package com.mashibing.juc.c_006; public class T implements Runnable {private int count = 10;public synchronized void run() { count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void main(String[] args) {for(int i=0; i<5; i++) {T t = new T();new Thread(t, "THREAD" + i).start();}}}

如下代碼:同步方法和非同步方法是否可以同時調用?就是我有一個synchronized的m1方法,我調用m1的時候能不能調用m2,拿大腿想一想這個是肯定可以的,線程里面訪問m1的時候需要加鎖,可是訪問m2的時候我又不需要加鎖,所以允許執行m2。

這些小實驗的設計是比較考驗功力的,學習線程的時候自己要多動手進行試驗,任何一個理論,都可以進行驗證。

/***同步和非同步方法是否可以同時調用?* @author mashibing*/ package com.mashibing.juc.c_007; public class T {public synchronized void m1() { System.out.println(Thread.currentThread().getName() + " m1 start...");try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " m1 end");}public void m2() {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " m2 ");}public static void main(String[] args) {T t = new T();/*new Thread(()->t.m1(), "t1").start();new Thread(()->t.m2(), "t2").start();*/new Thread(t::m1, "t1").start();new Thread(t::m2, "t2").start();/*//1.8之前的寫法new Thread(new Runnable() {@Overridepublic void run() {t.m1();}});*/} }

我們在來看一個synchronized應用的例子

我們定義了一個class賬戶,有名稱、余額。寫方法給哪個用戶設置它多少余額,讀方法通過這個名字得到余額值。如果我們給寫方法加鎖,給讀方法不加鎖,你的業務允許產生這種問題嗎?業務說我中間讀到了一些不太好的數據也沒關系,如果不允許客戶讀到中間不好的數據那這個就有問題。正因為我們加了鎖的方法和不加鎖的方法可以同時運行。

問題比如說:張三,給他設置100塊錢啟動了,睡了1毫秒之后呢去讀它的值,然后再睡2秒再去讀它的值這個時候你會看到讀到的值有問題,原因是在設定的過程中this.name你中間睡了一下,這個過程當中我模擬了一個線程來讀,這個時候調用的是getBalance方法,而調用這個方法的時候是不用加鎖的,所以說我不需要等你整個過程執行完就可以讀到你中間結果產生的內存,這個現象就叫做臟讀。這問題的產生就是synchronized方法和非synchronized方法是同時運行的。解決就是把getBalance加上synchronized就可以了,如果你的業務允許臟讀,就可以不用加鎖,加鎖之后的效率低下。

/*** 面試題:模擬銀行賬戶* 對業務寫方法加鎖* 對業務讀方法不加鎖* 這樣行不行?** 容易產生臟讀問題(dirtyRead)*/ package com.mashibing.juc.c_008;import java.util.concurrent.TimeUnit;public class Account {String name;double balance;public synchronized void set(String name, double balance) {this.name = name;try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}this.balance = balance;}public /*synchronized*/ double getBalance(String name) {return this.balance;}public static void main(String[] args) {Account a = new Account();new Thread(()->a.set("zhangsan", 100.0)).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(a.getBalance("zhangsan"));try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(a.getBalance("zhangsan"));} }

再來看synchronized的另外一個屬性:可重入,是synchronized必須了解的一個概念。

如果是一個同步方法調用另外一個同步方法,有一個方法加了鎖,另外一個方法也需要加鎖,加的是同一把鎖也是同一個線程,那這個時候申請仍然會得到該對象的鎖。比如說是synchronized可重入的,有一個方法m1 是synchronized有一個方法m2也是synchrionzed,m1里能不能調m2。我們m1開始的時候這個線程得到了這把鎖,然后在m1里面調用m2,如果說這個時候不允許任何線程再來拿這把鎖的時候就死鎖了。這個時候調m2它發現是同一個線程,因為你m2也需要申請這把鎖,它發現是同一個線程申請的這把鎖,允許,可以沒問題,這就叫可重入鎖。

/*** 一個同步方法可以調用另外一個同步方法,一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖。* 也就是說synchronized獲得鎖是可重入的* synchronized* @author mashibing*/ package com.mashibing.juc.c_009;import java.util.concurrent.TimeUnit;public class T {synchronized void m1() {System.out.println("m1 start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}m2();System.out.println("m1 end");}synchronized void m2() {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m2");}public static void main(String[] args) {new T().m1();} }

模擬一個父類子類的概念,父類synchronized,子類調用super.m的時候必須得可重入,否則就會出問題(調用父類是同一把鎖)。所謂的重入鎖就是你拿到這把鎖之后不停加鎖加鎖,加好幾道,但鎖定的還是同一個對象,去一道就減個1,就是這么個概念。

package com.mashibing.juc.c_010;import java.util.concurrent.TimeUnit;public class T {synchronized void m() {System.out.println("m start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m end");}public static void main(String[] args) {new TT().m();} }class TT extends T {@Overridesynchronized void m() {System.out.println("child m start");super.m();System.out.println("child m end");} }

線程池大小設置多少合適?


第一天 - 筆記

2019大廠面試題

為什么大廠喜歡問這些用不到的東西?


CAS

compare and swap(compare and exchange),即比較再交換
CAS算法基本流程就是:
1、獲取舊的值
2、計算
3、寫入值之前拿舊的值與最新的值比較,若相等說明未被他他線程改變,可以執行寫入操作。否則重新取出最新的值循環進行以上操作。

ABA問題
有這么一種場景:oldValue=0,nowValue被cpu2改為1,又被cpu2或其他線程改成了0。雖然oldValue=nowValue=0,但是這個0已經不是之前的0了,但是cpu1無感知,于是進行寫入操作。

并不是這種場景一定會給程序結果帶來影響。但是在某些特殊情況下,中間值的變動對業務是有影響的。如何解決這個問題?JDK源碼對于ABA問題的解決方式,有的是加版本號(AtomicStampedReference),有的是加bool類型的標志。

incrementAndGet()是原子性的,結果一定是1000000

incrementAndGet()調用了unsafe類的getAndGetInt()

compareAndSwapInt()

compareAndSwapInt()底層是用C和C++實現的

看一下C源碼

看一下最終實現:

最終翻譯成的匯編指令是: lock cmpxhg

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

cmpxchg 不具有原子性(先比較,再寫入,過程中間會有被打斷可能),lock指令在執行后面指令的時候鎖定一個北橋電信號,當執行cmpxchg 其他cpu不允許做修改,所以lock cmpxchg具有原子性。
所以cas還是會上鎖,不過鎖定北橋信號(不采用鎖總線的方式)比鎖定總線輕量,這就很好的解釋了場景C同時寫入問題。

一個對象在內存中的存儲:

將對象內存打印出來,示例:

打印結果:可以看到鎖定之后,markword被改變

MarkWord在64位機器上記錄信息的結構

MarkWord是一個8字節的頭。

問:MarkWord里面記錄了什么?
答:鎖信息、GC信息、HashCode

“無鎖”的含義是沒有“內核狀態的鎖”,也就是CAS

鎖升級初步


為了操作系統的安全,分為用戶態、內核態:所有用戶態的程序想要訪問硬件,需要內核態來調用。內核態拿到結果之后,再返回到用戶態。(P9面試題:0x80中斷過程是怎么實現的?)

偏向鎖

偏向鎖:將當前線程的指針貼在markword上。

為何會有偏向鎖?StringBuffer里面的方法,都是線程安全的。大多數時間,往往只有一個線程去操作它。它們的實現是偏向鎖。

當有多個線程競爭時,把偏向鎖撤下來,上自旋鎖(輕量級鎖):假如有一個線程搶到了這個鎖(使用CAS的方式,看值在準備寫入前與最初是否相同),就寫入。也叫樂觀鎖、忙等待。

競爭更激烈,即自旋超過10次時,升級為重量級鎖


jclasslib插件可以查看字節碼

偏向鎖默認是啟動的,延遲是4000ms。因為JVM啟動的時候內部有很多需要上鎖的過程,這個過程明確知道會有很多線程競爭,直接上輕量級鎖。

偏向鎖一定比自旋鎖的效率高嗎?
明確知道一個資源會有多個線程去競爭的時候,應該不啟用偏向鎖,直接使用輕量級鎖,避免鎖撤銷過程。

設置和偏向鎖相關的參數:
java -XXPrintFlagsFinal -version



Java是編譯+解釋執行都有。
JIT即時編譯:需要重復執行的熱點代碼編譯執行

lock addl實現內存屏障

總結

以上是生活随笔為你收集整理的【Java】多线程SynchronizedVolatile、锁升级过程 - 预习+第一天笔记的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。