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