熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

Java線程:線程的同步與鎖

2022-06-13   來源: Java高級技術 

  一同步問題提出

  線程的同步是為了防止多個線程訪問一個數據對象時對數據造成的破壞

  例如兩個線程ThreadAThreadB都操作同一個對象Foo對象並修改Foo對象上的數據

  public class Foo {

  private int x = ;

  public int getX() {

  return x;

  }

  public int fix(int y) {

  x = x y;

  return x;

  }

  }

  public class MyRunnable implements Runnable {

  private Foo foo = new Foo()

  public static void main(String[] args) {

  MyRunnable r = new MyRunnable()

  Thread ta = new Thread(r ThreadA

  Thread tb = new Thread(r ThreadB

  tastart()

  tbstart()

  }

  public void run() {

  for (int i = ; i < ; i++) {

  thisfix(

  try {

  Threadsleep(

  } catch (InterruptedException e) {

  eprintStackTrace()

  }

  Systemoutprintln(ThreadcurrentThread()getName() + : 當前foo對象的x值= + foogetX())

  }

  }

  public int fix(int y) {

  return foofix(y)

  }

  }

  運行結果

  ThreadA : 當前foo對象的x值=

  ThreadB : 當前foo對象的x值=

  ThreadB : 當前foo對象的x值=

  ThreadA : 當前foo對象的x值=

  ThreadA : 當前foo對象的x值=

  ThreadB : 當前foo對象的x值=

  Process finished with exit code

  從結果發現這樣的輸出值明顯是不合理的原因是兩個線程不加控制的訪問Foo對象並修改其數據所致

  如果要保持結果的合理性只需要達到一個目的就是將對Foo的訪問加以限制每次只能有一個線程在訪問這樣就能保證Foo對象中數據的合理性了

  在具體的Java代碼中需要完成一下兩個操作

  把競爭訪問的資源類Foo變量x標識為private;

  同步哪些修改變量的代碼使用synchronized關鍵字同步方法或代碼

  二同步和鎖定

  鎖的原理

  Java中每個對象都有一個內置鎖

  當程序運行到非靜態的synchronized同步方法上時自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖獲得一個對象的鎖也稱為獲取鎖鎖定對象在對象上鎖定或在對象上同步

  當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起作用

  一個對象只有一個鎖所以如果一個線程獲得該鎖就沒有其他線程可以獲得鎖直到第一個線程釋放(或返回)鎖這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊直到該鎖被釋放

  釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊

  關於鎖和同步有一下幾個要點

  只能同步方法而不能同步變量和類

  每個對象只有一個鎖當提到同步時應該清楚在什麼上同步?也就是說在哪個對象上同步?

  不必同步類中所有的方法類可以同時擁有同步和非同步方法

  如果兩個線程要執行一個類中的synchronized方法並且兩個線程使用相同的實例來調用方法那麼一次只能有一個線程能夠執行方法另一個需要等待直到鎖被釋放也就是說如果一個線程在對象上獲得一個鎖就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法

  如果線程擁有同步和非同步方法則非同步方法可以被多個線程自由訪問而不受鎖的限制

  線程睡眠時它所持的任何鎖都不會釋放

  線程可以獲得多個鎖比如在一個對象的同步方法裡面調用另外一個對象的同步方法則獲取了兩個對象的同步鎖

  同步損害並發性應該盡可能縮小同步范圍同步不但可以同步整個方法還可以同步方法中一部分代碼塊

  在使用同步代碼塊時候應該指定在哪個對象上同步也就是說要獲取哪個對象的鎖例如

  public int fix(int y) {

  synchronized (this) {

  x = x y;

  }

  return x;

  }

  當然同步方法也可以改寫為非同步方法但功能完全一樣的例如

  public synchronized int getX() {

  return x++;

  }

  與

  public int getX() {

  synchronized (this) {

  return x;

  }

  }

  效果是完全一樣的

  三靜態方法同步

  要同步靜態方法需要一個用於整個類對象的鎖這個對象是就是這個類(XXXclass)

  例如

  public static synchronized int setName(String name){

  Xxxname = name;

  }

  等價於

  public static int setName(String name){

  synchronized(Xxxclass){

  Xxxname = name;

  }

  }

  四如果線程不能不能獲得鎖會怎麼樣

  如果線程試圖進入同步方法而其鎖已經被占用則線程在該對象上被阻塞實質上線程進入該對象的的一種池中必須在哪裡等待直到其鎖被釋放該線程再次變為可運行或運行為止

  當考慮阻塞時一定要注意哪個對象正被用於鎖定

  調用同一個對象中非靜態同步方法的線程將彼此阻塞如果是不同對象則每個線程有自己的對象的鎖線程間彼此互不干預

  調用同一個類中的靜態同步方法的線程將彼此阻塞它們都是鎖定在相同的Class對象上

  靜態同步方法和非靜態同步方法將永遠不會彼此阻塞因為靜態方法鎖定在Class對象上非靜態方法鎖定在該類的對象上

  對於同步代碼塊要看清楚什麼對象已經用於鎖定(synchronized後面括號的內容)在同一個對象上進行同步的線程將彼此阻塞在不同對象上鎖定的線程將永遠不會彼此阻塞

  五何時需要同步

  在多個線程同時訪問互斥(可交換)數據時應該同步以保護數據確保兩個線程不會同時修改更改它

  對於非靜態字段中可更改的數據通常使用非靜態方法訪問

  對於靜態字段中可更改的數據通常使用靜態方法訪問

  如果需要在非靜態方法中使用靜態字段或者在靜態字段中調用非靜態方法問題將變得非常復雜已經超出SJCP考試范圍了

  六線程安全類

  當一個類已經很好的同步以保護它的數據時這個類就稱為線程安全的

  即使是線程安全類也應該特別小心因為操作的線程是間仍然不一定安全

  舉個形象的例子比如一個集合是線程安全的有兩個線程在操作同一個集合對象當第一個線程查詢集合非空後刪除集合中所有元素的時候第二個線程也來執行與第一個線程相同的操作也許在第一個線程查詢後第二個線程也查詢出集合非空但是當第一個執行清除後第二個再執行刪除顯然是不對的因為此時集合已經為空了

  看個代碼

  public class NameList {

  private List nameList = CollectionssynchronizedList(new LinkedList())

  public void add(String name) {

  nameListadd(name)

  }

  public String removeFirst() {

  if (nameListsize() > ) {

  return (String) nameListremove(

  } else {

  return null;

  }

  }

  }

  public class Test {

  public static void main(String[] args) {

  final NameList nl = new NameList()

  nladd(aaa

  class NameDropper extends Thread{

  public void run(){

  String name = nlremoveFirst()

  Systemoutprintln(name)

  }

  }

  Thread t = new NameDropper()

  Thread t = new NameDropper()

  tstart()

  tstart()

  }

  }

  雖然集合對象

  private List nameList = CollectionssynchronizedList(new LinkedList())

  是同步的但是程序還不是線程安全的

  出現這種事件的原因是上例中一個線程操作列表過程中無法阻止另外一個線程對列表的其他操作

  解決上面問題的辦法是在操作集合對象的NameList上面做一個同步改寫後的代碼如下

  public class NameList {

  private List nameList = CollectionssynchronizedList(new LinkedList())

  public synchronized void add(String name) {

  nameListadd(name)

  }

  public synchronized String removeFirst() {

  if (nameListsize() > ) {

  return (String) nameListremove(

  } else {

  return null;

  }

  }

  }

  這樣當一個線程訪問其中一個同步方法時其他線程只有等待

  七線程死鎖

  死鎖對Java程序來說是很復雜的也很難發現問題當兩個線程被阻塞每個線程在等待另一個線程時就發生死鎖

  還是看一個比較直觀的死鎖例子

  public class DeadlockRisk {

  private static class Resource {

  public int value;

  }

  private Resource resourceA = new Resource()

  private Resource resourceB = new Resource()

  public int read() {

  synchronized (resourceA) {

  synchronized (resourceB) {

  return resourceBvalue + resourceAvalue;

  }

  }

  }

  public void write(int a int b) {

  synchronized (resourceB) {

  synchronized (resourceA) {

  resourceAvalue = a;

  resourceBvalue = b;

  }

  }

  }

  }

  假設read()方法由一個線程啟動write()方法由另外一個線程啟動讀線程將擁有resourceA鎖寫線程將擁有resourceB鎖兩者都堅持等待的話就出現死鎖

  實際上上面這個例子發生死鎖的概率很小因為在代碼內的某個點CPU必須從讀線程切換到寫線程所以死鎖基本上不能發生

  但是無論代碼中發生死鎖的概率有多小一旦發生死鎖程序就死掉有一些設計方法能幫助避免死鎖包括始終按照預定義的順序獲取鎖這一策略已經超出SCJP的考試范圍

  八線程同步小結

  線程同步的目的是為了保護多個線程反問一個資源時對資源的破壞

  線程同步方法是通過鎖來實現每個對象都有切僅有一個鎖這個鎖與一個特定的對象關聯線程一旦獲取了對象鎖其他訪問該對象的線程就無法再訪問該對象的其他同步方法

  對於靜態同步方法鎖是針對這個類的鎖對象是該類的Class對象靜態和非靜態方法的鎖互不干預一個線程獲得鎖當在一個同步方法中訪問另外對象上的同步方法時會獲取這兩個對象鎖

  對於同步要時刻清醒在哪個對象上同步這是關鍵

  編寫線程安全的類需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷原子操作做出分析並保證原子操作期間別的線程無法訪問競爭資源

  當多個線程等待一個對象鎖時沒有獲取到鎖的線程將發生阻塞

  死鎖是線程間相互等待鎖鎖造成的在實際中發生的概率非常的小真讓你寫個死鎖程序不一定好使呵呵但是一旦程序發生死鎖程序將死掉


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27281.html
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.