java动态同步_java并发基础-Synchronized
基礎(chǔ)使用
基本上Java程序員都簡單的了解synchronized的使用: 無非就是用在多線程環(huán)境下的同步。 看如下簡單的例子:
publicclassUnsafeCounter{
privateint count=0;
publicint getAndIncrement(){
returnthis.count++;
}
}
上面是一個簡單的非常常見的POJO類,在多線程環(huán)境下的測試代碼:
publicclassRunUnsafeCounter{
privatestaticfinalUnsafeCounterunsafeCounter=newUnsafeCounter();
publicstaticvoidunsafeCounter()throwsInterruptedException{
inti=0;
ListthreadList=newArrayList<>(1025);
while(i<1000){
threadList.add(newThread(newRunnable(){
@Override
publicvoidrun(){
System.out.println(Thread.currentThread().getName()+" : "+unsafeCounter.getAndIncrement());;
}
}));
i++;
}
threadList.forEach(thread->thread.start());
for(Threadthread:threadList){
thread.join();
}
}
publicstaticvoidmain(String[]args){
for(inti=0;i<10;i++){
try{
RunUnsafeCounter.unsafeCounter();
System.out.println(unsafeCounter.getCount());
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
}
上面的測試類中有一個靜態(tài)的UnsafeCounter實(shí)例,然后生成了1000個線程調(diào)用非線程安全的getAndIncrement方法, 按照平常單線程環(huán)境的結(jié)果,這里的值應(yīng)該是1000. 但是運(yùn)行RunCounter類就會發(fā)現(xiàn)結(jié)果不一定是1000并且每一次的結(jié)果都不一定相同。 這是因?yàn)槎鄠€線程同時訪問getAndIncrement這一個非線程安全的方法,可能中間某幾個線程可能同時在運(yùn)行這個方法,然后在進(jìn)行++操作時,某個線程獲取到了當(dāng)前值,結(jié)果又切換到了其他線程也獲取到了當(dāng)前值,然后這兩個線程的++得到了相同的結(jié)果。 也就導(dǎo)致了最終結(jié)果的不確定性。
再看如下使用synchronized已保證線程安全性的代碼:
publicclassSafeCounter{
privateintcount=0;
publicsynchronizedintgetCount(){
returncount;
}
publicsynchronizedintgetAndIncrement(){
returnthis.count++;
}
}
上面的POJO類的getAndIncrement方法使用synchronized修飾,而且getCount方法也使用synchronized修飾。 測試?yán)?#xff1a;
publicclassRunSafeCounter{
privatestaticfinalSafeCountersafeCounter=newSafeCounter();
publicstaticvoidsafeCounter()throwsInterruptedException{
inti=0;
ListthreadList=newArrayList<>();
while(i<1000){
threadList.add(newThread(newRunnable(){
@Override
publicvoidrun(){
safeCounter.getAndIncrement();
}
}));
i++;
}
threadList.forEach(thread->thread.start());
for(Threadthread:threadList){
thread.join();
}
}
publicstaticvoidmain(String[]args){
try{
for(inti=0;i<10;i++){
RunSafeCounter.safeCounter();
System.out.println(safeCounter.getCount());
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
而上面的代碼在經(jīng)過10*1000次循環(huán)過后獲得結(jié)果是10000, 無論重復(fù)多少次都是。 并且也保證了線程的安全性。(PS: 在看完下面的內(nèi)容過后判斷SafeCounter中的Getter方法的getCount方法如果去掉synchronized修飾會不會還是一樣的結(jié)果?)
規(guī)范說明
Java為多線程之間通信提供了非常多的機(jī)制,而其中, Synchronized是最基礎(chǔ)最簡單的一個。在JLS-17 對 Synchronized的定義大概意思如下(ps為我加的備注,非原文):
synchoronized使用監(jiān)視器實(shí)現(xiàn)。 Java中每一個對象都和一個監(jiān)視器關(guān)聯(lián),線程可以鎖或則解鎖監(jiān)視器, 同一時間只有一個線程持有監(jiān)視器的鎖,其他任何想獲取該監(jiān)視器鎖的線程都會被阻塞知道可以獲得該鎖(ps: 擁有鎖的線程釋放過后)。 一個線程可能對一個監(jiān)視器鎖多次(ps: 可重入),每一個解鎖恢復(fù)一次鎖操作(ps: 內(nèi)部維護(hù)一個監(jiān)視器鎖的次數(shù),每退出一個減少1直到為0就釋放該監(jiān)視器的鎖)
synchoronized塊計算一個對象的引用,然后開始在對象的監(jiān)視器上執(zhí)行鎖操作并且不繼續(xù)向下執(zhí)行直到鎖操作成功后。然后,synchoronized塊的內(nèi)容開始執(zhí)行。 如果塊中的內(nèi)容執(zhí)行完成(不管是正常還是突然(ps: 被外部關(guān)閉之類)),在該監(jiān)視器上就會自動執(zhí)行解鎖操作。
synchoronized方法在調(diào)用它的時候自動執(zhí)行鎖操作。它的方法內(nèi)容在成功獲取到鎖之前不會執(zhí)行,如果是實(shí)例方法,它鎖住了調(diào)用它的實(shí)例的監(jiān)視器(方法中的this),如果是靜態(tài)方法,它鎖住了定義這個方法的類的Class對象的監(jiān)視器, 如果塊中的內(nèi)容執(zhí)行完成(不管是正常還是突然(ps: 被外部關(guān)閉之類)),在該監(jiān)視器上就會自動執(zhí)行解鎖操作。
Java語言既不預(yù)防也不檢查死鎖(ps:這是程序員的事)
其他機(jī)制,比如volatile的讀和寫或則java.util.concurrent包提供了其他的可替代的同步方式。
一個synchronized塊請求一個互斥鎖,當(dāng)擁有鎖的線程在執(zhí)行時,其他線程要獲取這個鎖必須等待。 它的語法如下:
SynchronizedStatement:
synchronized (Expression) Block
表達(dá)式的類型必須為引用類型,否則編譯期報錯。該方法塊 首先計算表達(dá)式的值,然后執(zhí)行其中的代碼。** 如果計算表達(dá)式突然結(jié)束,那么代碼塊已同樣的理由突然結(jié)束。 如果是null,就會拋出空指針異常。** 否則,就獲取到表達(dá)式值鎖代表的對象的監(jiān)視器的鎖,然后開始執(zhí)行同步代碼塊。 如果代碼塊正常退出,監(jiān)視器就會被解鎖然后synchronized塊也正常退出。 如果是已其他任何理由突然中斷的話,監(jiān)視器會被解鎖并且同步代碼塊會已同樣的方式結(jié)束。
一個synchronized方法在運(yùn)行之前會先請求一個監(jiān)視器(的鎖)。對于一個靜態(tài)方法,該類的Class對象關(guān)聯(lián)的監(jiān)視器將被獲取。 對于一個實(shí)例方法, this所代表的實(shí)例的監(jiān)視器將被獲取。
同樣,在JLS中也寫清楚了每一個對象關(guān)聯(lián)的監(jiān)視器都有一個Wait Sets,顯而易見的就是用來保存當(dāng)前等待獲取當(dāng)前監(jiān)視器鎖的線程集合。該集合僅僅可以被Object.wait , Object.notify ,Object.notifyAll操縱。
synchoronized保證的互斥性與鎖的對象
當(dāng)然,對于synchoronized來講,它的具體的規(guī)范可以閱讀一下,但也沒有必要在這里完全照搬過來。 在理解了上篇的Java內(nèi)存模型并且仔細(xì)閱讀了上面的JLS中synchoronized的定義過后,對于在程序中如何正確的使用其實(shí)應(yīng)該有了個基本的概念。 我認(rèn)為,使用synchoronized,最基本也是最重要的就是:
你為什么需要用synchoronized?
你鎖的究竟是哪個對象?
為了做什么?
考慮如下代碼:
publicclassSynchronizedCounter{
privateintc=0;
publicsynchronizedvoidincrement1(){
c++;
}
publicvoidincrement2(){
synchronized(this){
c++;
}
}
}
對于increment1方法,它是一個同步方法,并且是實(shí)例方法。 根據(jù)上面的定義,該方法會獲取調(diào)用該方法的實(shí)例的監(jiān)視器的鎖; 而對于increment2,它是一個同步代碼塊,但獲取一個對象的引用,然后嘗試獲取鎖。 這里的引用是this,其實(shí)也就是該調(diào)用increment2的實(shí)例。 所以說, increment1和increment2其實(shí)是做了完全一樣的事情。
代碼:
classTest{
intcount;
synchronizedvoidbump(){
count++;
}
staticintclassCount;
staticsynchronizedvoidclassBump(){
classCount++;
}
}
與
classBumpTest{
intcount;
voidbump(){
synchronized(this){count++;}
}
staticintclassCount;
staticvoidclassBump(){
try{
synchronized(Class.forName("BumpTest")){
classCount++;
}
}catch(ClassNotFoundExceptione){}
}
}
也是擁有相同的效果。
在搞清楚鎖的對象和時間周期過后,下面代碼的安全性應(yīng)該很容易看出來了:
publicclassLockObjectTest{
privatestaticintindex=0;
publicsynchronizedintgetAndIncrement1(){//這個鎖的是實(shí)例的監(jiān)視器
returnindex++;
}
publicstaticsynchronizedintgetAndIncrement2(){//這個鎖的是LockObjectTest類的Class對象的監(jiān)視器
returnindex++;
}
publicstaticvoidmain(String[]args){
inti=0;
ListthreadList=newArrayList<>(1000);
LockObjectTestlockObjectTest=newLockObjectTest();
while(i<10000){
i++;
threadList.add(newThread(newRunnable(){
@Override
publicvoidrun(){
lockObjectTest.getAndIncrement1();
}
}));
threadList.add(newThread(newRunnable(){
@Override
publicvoidrun(){
LockObjectTest.getAndIncrement2();
}
}));
}
threadList.forEach(thread->thread.start());
try{
for(Threadthread:threadList){
thread.join();
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println(LockObjectTest.index);
}
}
synchoronized保證的內(nèi)存可見性
當(dāng)線程A執(zhí)行一個同步代碼塊過后,線程B進(jìn)入同一個監(jiān)視器鎖的同步代碼快的時候,所在線程A的操作(特別是對變量的改變)都保證可以被線程B看到(即不會因?yàn)橹嘏判蚧騽t緩存之類的影響而看到錯誤的值)
內(nèi)存可見性在單線程環(huán)境下從來沒有出現(xiàn)過,因?yàn)檫@似乎就是一個智障問題:我前面給變量賦值了,后面肯定可以看到這個值。不然我們的代碼豈不是問題重重?
而在多線程環(huán)境下之所以會出現(xiàn)這個問題還是由于編譯器、運(yùn)行時、CPU共同作用的結(jié)果。
編譯器(不一定指javac,JIT)會對代碼進(jìn)行優(yōu)化,一個非常常見的就是編譯器循環(huán)優(yōu)化,知乎RednaxelaFX的一個回答。 編譯器在編譯的時候可能就已經(jīng)改變了代碼中的變量聲明或則賦值順序-只要保證了語義一致性。 R大已經(jīng)解釋的非常清楚。
現(xiàn)代處理器的亂序執(zhí)行和CPU上越來越多的緩存(L1,L2,L3 cache)都使得你最終跑在CPU上的代碼和你所寫的出入較大。 多線程環(huán)境下尤其需要考慮這種影響。 比如下面的代碼:
intarith(intx,inty,intz){
intt1=x+y;
intt2=z*48;
intt3=t1&0xFFFF;
intt4=t2*t3;
returnt4;
}
由于t1和t2的賦值互不影響,所以他們的順序完全可能已隨機(jī)的次序跑在CPU上。
而內(nèi)存可見性其實(shí)也是這個道理。 當(dāng)你的程序跑在同一個線程的時候,后面的代碼讀取之前對變量的更改都會是在同一個“核心”的寄存器或則緩存上。 而如果是多線程環(huán)境,假設(shè)某一個線程更改了某個變量,然后放到了它的寄存器上。 而另外一個線程此時來讀取這個變量,它是會從內(nèi)存中讀取還是從這個“核心”的緩存中讀取還是從這個“核心”的寄存器上讀取、又或則由于重排序這里的賦值還沒有發(fā)生 是不能得到保證的。而唯一可以確定的是,它讀取到的總會是某個線程在某個時間更改的數(shù)據(jù),這被稱為最低保證性(JMM規(guī)定了64位的數(shù)值(double,long)可以分成2個32位的進(jìn)行計算,也就是說這兩種數(shù)據(jù)類型沒有最低保證性。它們的數(shù)據(jù)完全可能是隨機(jī)的)。
如下代碼:
publicclassNoVisibility{
privatestaticbooleanready;
privatestaticintnumber;
privatestaticclassReaderThreadextendsThread{
publicvoidrun(){
while(!ready)
Thread.yield();
System.out.println(number);
}
}
publicstaticvoidmain(String[]args){
newReaderThread().start();
number=42;
ready=true;
}
}
上面代碼主線程和讀線程訪問共享變量ready和number,主線程開始讀線程,然后把number設(shè)為42,把ready設(shè)置為true。 讀線程等待ready為true后打印number. 但是這里,讀線程可能會看到number是42也可能是0,或者說是永遠(yuǎn)不終止。主線程對于ready和number的寫不能保證可以被其他線程看到。
synchronized可以保證內(nèi)存可見性,也就是使用了synchronized關(guān)鍵字的方法或則語句都可以保證內(nèi)存可見性(還有其他機(jī)制,如volatile)。具體的細(xì)節(jié)并發(fā)編程網(wǎng)有一篇非常好的文章
當(dāng)線程A運(yùn)行一個synchronized塊,然后之后線程B進(jìn)入同一個鎖的synchronized塊時,線程A釋放鎖之前可見的變量可以保證在線程B獲取鎖的時候可以看見。 換句話說,線程A做的事情線程B都知道。 而沒有synchronized,則沒有這樣的保證。
總結(jié)
以上是生活随笔為你收集整理的java动态同步_java并发基础-Synchronized的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 重命名索引_mysql增删改
- 下一篇: java 反射 类名_java – 从