溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何在Java中使用synchronized

發(fā)布時間:2021-05-17 17:36:54 來源:億速云 閱讀:122 作者:Leah 欄目:編程語言

這篇文章給大家介紹如何在Java中使用synchronized,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

生活中隨處可見并行的例子,并行 顧名思義就是一起進行的意思,同樣的程序在某些時候也需要并行來提高效率,在上一篇文章中我們了解了 Java 語言對緩存導致的可見性問題、編譯優(yōu)化導致的順序性問題的解決方法,下面我們就來看看 Java 中解決因線程切換導致的原子性問題的解決方案 -- 鎖 。

說到鎖我們并不陌生,日常工作中也可能經常會用到,但是我們不能只停留在用的層面上,為什么要加鎖,不加鎖行不行,不行的話會導致哪些問題,這些都是在使用加鎖語句時我們需要考慮的。

來看一個使用 32 位的 CPU 寫 long 型變量需不需要加鎖的問題:

我們知道 long 型變量長度為 64 位,在 32 位 CPU 上寫 long 型變量至少需要拆分成 2 個步驟:一次寫 高 32 位,一次寫低 32 位。

對于單核 CPU 來說,同一時刻只有一個線程在執(zhí)行,禁止 CPU 中斷就意味著禁止線程切換,獲得 CPU 使用權的這個線程就會一直運行,所以 2 次寫操作要么同時都被執(zhí)行,要么都不被執(zhí)行,單核 CPU 是保證原子性的。

對于多核 CPU,同一時刻,一個線程在 CPU-1 上運行,另一個線程在 CPU-2 上運行,此時禁止 CPU 切換,只能保證 CPU 上有線程運行,并不能保證同一時刻只有一個線程運行,如果兩個線程同時都在寫高位,那么得出的結果可就不正確了。

所以,互斥修改共享變量這個條件非常重要,也就是說同一時刻只有一個線程在修改共享變量,只要保證這個條件,不論單核還是多核,操作就都是原子性的了。

一說到互斥、原子性,我們馬上就想到了代碼加鎖,沒錯加鎖是正確的選擇,但是怎么加呢? 要想知道怎么加鎖,首先我們要知道加鎖鎖的是什么以及我們想要保護的資源是什么,看下圖說說鎖的是什么,要保護的是什么呢?

如何在Java中使用synchronized

圖中鎖的 M 資源,保護的也是 M 資源。

程序中的鎖與現實中的鎖也是類似的,每一把鎖都有自己要保護的資源,這是至關重要的,如圖保護資源 M 的鎖為 LM,就像我家大門的鎖保護我家,你家大門的鎖保護你家一樣,如果程序出現類似我家大門鎖保護你家的情況,那么就會導致詭異的并發(fā)問題了。

了解了鎖的是什么與保護的是什么之后,我們看看怎么加鎖的問題,還是用 count += 1 的例子,看代碼:

class Test{
 long value = 0L;
 long get() {
  return value;
 }
 synchronized void addOne() {
  value += 1;
 }
}

分析一下,這段代碼中鎖的是當前對象,要保護的資源是對象中的成員屬性 value,這樣的加鎖方式開啟10 個線程分別調用 10000次 addOne()方法,我們預期的結果是 value 最終會達到 100000,結果如何呢 ?

經過測試,addOne() 不加 synchronized 結果會出現小于 100000 的情況,加上 synchronized 結果符合我們的預期,針對測試結果,簡要分析如下:

加鎖之后,線程之間是互斥的,也就是說同一時刻只有一個線程執(zhí)行,這樣就原子性可以保證了。

那么可見性呢?一個線程操作結束后另一個線程能獲取到上一個線程的操作結果嗎?答案是肯定的,這就跟我們上一章說的 happen before 原則聯系到一起了,“一個鎖的解鎖操作對另一個鎖的加鎖操作是可見的”,再結合傳遞性規(guī)則,一個鎖在解鎖前,對共享變量的修改,即解鎖前對共享變量修改 happen before 于 這個鎖的解鎖,這個鎖的解鎖操作 happen before于另一個鎖的加鎖。

所以,解鎖前對共享變量修改happen before于另一個鎖的加鎖,也就是說解鎖前對共享變量修改對于另一個鎖的加鎖是可見的。

到這一切看似還挺完美,其實我們忽略了 get() 方法,多線程操作 get() 方法會是安全的嗎?在沒有任何前提操作的情況下,直接調用 get() 方法當然沒問題,就是取值又不涉及修改。但是如果在執(zhí)行 addOne() 方法后調用呢?顯然,這時候 value 值的修改對 get() 方法是不可見的,happen before 中只說了鎖的規(guī)則,這里要想保證可見性,對 get()方法也需要加上一把鎖。代碼如下:

class Test{
 long value = 0L;
 synchronized long get() {
  return value;
 }
 synchronized void addOne() {
  value += 1;
 }
}

這里我們用同一把鎖,保護了共享資源 value。說到這,我們根據資源關系來將使用鎖的情況分為兩種:

保護沒有關系的多個資源

保護有關系的多個資源

對于 1 的情況,由于屬性之間沒有關系,每個資源都用一把鎖來控制,例如修改賬戶的密碼、修改余額操作,密碼與余額是沒有關系的資源,分別用兩把鎖來控制即可,這種鎖叫做細粒度鎖,使用不同的鎖對受保護的資源進行精細化管理,可以提升性能。

對于 2 的情況 ,則需要粒度更大的鎖去保護多個資源,看下面這段代碼:

class Account {
 private int balance;
 // 轉賬
 synchronized void transfer(
   Account target, int amt){
  if (this.balance > amt) {
   this.balance -= amt;
   target.balance += amt;
  }
 } 
}

乍一看,沒問題,轉賬操作加了鎖,妥妥的。其實則不然,看圖就明白了:

如何在Java中使用synchronized

現在這就是"用我家鎖鎖了你家"的典型例子,這時候臨界區(qū)有多個資源,我們應該使用更大粒度的鎖,看看這樣改怎么樣:

class Account {
 private int balance;
 // 轉賬
 void transfer(Account target, int amt){
  synchronized(Account.class) {
   if (this.balance > amt) {
    this.balance -= amt;
    target.balance += amt;
   }
  }
 } 
}

這里我們用 Account.class 作為更大粒度的鎖是可行的, class 就是我們常說的 “類模板”,在 JVM 中只會加載一次,所以所有 Account 對象的類模板都是相同的,這樣就能夠保證用一把大鎖鎖住了有關系的共享資源。

問題是解決了,仔細一想,如果用 Account.class 作為鎖,那豈不是所有的轉賬操作都是串行了,這樣肯定是不行的,生活中轉賬肯定也不是串行的,如果串行那效率真的是很太差了。

正確的方式應該是這樣的:

class Account {
 //靜態(tài)屬性 替代 Account.class 作為一把大鎖
 private static Object lock = new Object();
 private int balance;
 // 轉賬
 void transfer(Account target, int amt){
  synchronized(lock) {
   if (this.balance > amt) {
    this.balance -= amt;
    target.balance += amt;
   }
  }
 }

常用的java框架有哪些

1.SpringMVC,Spring Web MVC是一種基于Java的實現了Web MVC設計模式的請求驅動類型的輕量級Web框架。2.Shiro,Apache Shiro是Java的一個安全框架。3.Mybatis,MyBatis 是支持普通 SQL查詢,存儲過程和高級映射的優(yōu)秀持久層框架。4.Dubbo,Dubbo是一個分布式服務框架。5.Maven,Maven是個項目管理和構建自動化工具。6.RabbitMQ,RabbitMQ是用Erlang實現的一個高并發(fā)高可靠AMQP消息隊列服務器。7.Ehcache,EhCache 是一個純Java的進程內緩存框架。

關于如何在Java中使用synchronized就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI