天天看點

2.髒讀與sync關鍵字細節問題

文章目錄

  • ​​髒讀​​
  • ​​擴充資料庫一緻性的例子(oracle)​​
  • ​​鎖重入的問題​​
  • ​​異常釋放鎖的機制​​
  • ​​sycn減小鎖的粒度​​
  • ​​對象鎖​​
  • ​​String鎖的問題​​
  • ​​鎖改變的問題​​
  • ​​死鎖​​

髒讀

對于對象的同步和異步方法,我們在設計自己程式員的時候,一定要考慮問題的整體性,不然就會出現資料不一緻的錯誤,很經典的錯誤就是髒讀。也就是資料的一緻性問題。存在多個線程工作(主線程,子線程),當一個線程(子線程)在設定值的時候,比如這個過程需要2秒,在1s時其他線程進來擷取值了。此時就産生了髒讀,讀取的資料是不正确的。

錯誤現象:因為一個用了同步,一個沒用同步,這實際上是一套關聯的業務(整體性),别人還沒改完,就不能讀。現象是先出來get,一秒之後出set。

解決的方法也比較簡單,兩個方法同時加上同步關鍵字。這樣就不會出現髒讀了,正常操作就是get/set方法需要加上關鍵字可以防止髒讀這也是一個思路,原理就是實作讀寫互斥。

package com.bjsxt.base.sync004;
/**
 * 業務整體需要使用完整的synchronized,保持業務的原子性。
 * @author alienware
 *
 */
public class DirtyRead {

  private String username = "bjsxt";
  private String password = "123";
  
  public synchronized void setValue(String username, String password){
    this.username = username;
    
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    this.password = password;
    
    System.out.println("setValue最終結果:username = " + username + " , password = " + password);
  }
  
  public void getValue(){
    System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
  }
  
  
  public static void main(String[] args) throws Exception{
    
    final DirtyRead dr = new DirtyRead();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        dr.setValue("z3", "456");   
      }
    });
    t1.start();
    Thread.sleep(1000);
    
    dr.getValue();
  }
  
  
  
}      

擴充資料庫一緻性的例子(oracle)

資料庫的中很多資料1000W。9點發了一條查詢語句,需要到9.10分才能查到那條資料(結果值為100)。但是在9.05分執行了一條DML語句(update),把值改為了200.請問select語句傳回的結果是?

傳回的結果一定是100。oracle資料庫有一緻性的概念(一緻性讀),9點的查詢語句,永遠看到的是9點那一瞬間的結果。(避免了髒讀)oracle中有undo的概念,好比日志,當用戶端執行dml(資料修改語句)語句時,會把老值放入undo,然後修改,如果失敗,則從undo中復原。9點的那天查詢語句,發現值對應的undo有變化(現在值和undo不一樣)了,就會去undo裡面找老值。如果能找到,就傳回,找不到就抛異常。甯肯抛異常,也不會把新值200給用戶端看、這個抛出的異常叫做snapshot too old的經典異常(快照太舊)。這個問題展現了oracle一緻性讀的特性。

鎖重入的問題

關鍵字sync有鎖重入的功能,也就是在使用sync時,當一個線程得到了一個對象的鎖之後,再次請求此對象時可以再次得到該對象的鎖。

/**
 * synchronized的重入
 *
 */
public class SyncDubbo1 {

  public synchronized void method1(){
    System.out.println("method1..");
    method2();
  }
  public synchronized void method2(){
    System.out.println("method2..");
    method3();
  }
  public synchronized void method3(){
    System.out.println("method3..");
  }
  
  public static void main(String[] args) {
    final SyncDubbo1 sd = new SyncDubbo1();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        sd.method1();
      }
    });
    t1.start();
  }
}      

除了上面這個案例還可以使用父子嵌套重入鎖,父類有個sync方法,子類有個sync方法,子類中的sync方法調用了父類的sync方法,這種也叫作重入鎖,利用sync關鍵字,也是線程安全的。

如果有一個沒用用關鍵字修飾,就是線程不安全的。

package com.bjsxt.base.sync005;
/**
 * synchronized的重入
 * @author alienware
 *
 */
public class SyncDubbo2 {

  static class Main {
    public int i = 10;
    public synchronized void operationSup(){
      try {
        i--;
        System.out.println("Main print i = " + i);
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  static class Sub extends Main {
    public synchronized void operationSub(){
      try {
        while(i > 0) {
          i--;
          System.out.println("Sub print i = " + i);
          Thread.sleep(100);    
          this.operationSup();
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  public static void main(String[] args) {
    
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        Sub sub = new Sub();
        sub.operationSub();
      }
    });
    
    t1.start();
  }
  
  
}      

異常釋放鎖的機制

被sync修飾的方法遇到異常,會釋放鎖,此時其他線程會進入。這種情況如果多個任務不相關還好。

但是一旦有業務關系,就需要妥善處理(記錄日志是必須的)。不處理的話可以加continue讓他繼續。如果出錯了,後面一定不能執行,就抛出一個運作時異常,讓他停止。

package com.bjsxt.base.sync005;
/**
 * synchronized异常
 * @author alienware
 *
 */
public class SyncException {

  private int i = 0;
  public synchronized void operation(){
    while(true){
      try {
        i++;
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + " , i = " + i);
        if(i == 20){
          //Integer.parseInt("a");
          throw new RuntimeException();
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  public static void main(String[] args) {
    
    final SyncException se = new SyncException();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        se.operation();
      }
    },"t1");
    t1.start();
  }
  
  
}      

sycn減小鎖的粒度

使用sync聲明的方法在某些情況下是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待比較長的時間才能執行,這樣的情況可以使用sync代碼塊去優化代碼執行的時間,也就是通常所說的減小鎖的粒度。

package com.bjsxt.base.sync006;

/**
 * 使用synchronized代碼塊減小鎖的粒度,提高性能
 *
 */
public class Optimize {

  public void doLongTimeTask(){
    try {
      
      System.out.println("目前線程開始:" + Thread.currentThread().getName() + 
          ", 正在執行一個較長時間的業務操作,其内容不需要同步");
      Thread.sleep(2000);
      
      synchronized(this){
        System.out.println("目前線程:" + Thread.currentThread().getName() + 
          ", 執行同步代碼塊,對其同步變量進行操作");
        Thread.sleep(1000);
      }
      System.out.println("目前線程結束:" + Thread.currentThread().getName() +
          ", 執行完畢");
      
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  
  public static void main(String[] args) {
    final Optimize otz = new Optimize();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        otz.doLongTimeTask();
      }
    },"t1");
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        otz.doLongTimeTask();
      }
    },"t2");
    t1.start();
    t2.start();
    
  }
  
  
}      

對象鎖

上述是一種使用目前對象加鎖的方式,事實上sync可以使用任意的object對象來加鎖。

/**
 * 使用synchronized代碼塊加鎖,比較靈活
 * @author alienware
 *
 */
public class ObjectLock {

  public void method1(){
    synchronized (this) { //對象鎖
      try {
        System.out.println("do method1..");
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  public void method2(){    //類鎖
    synchronized (ObjectLock.class) {
      try {
        System.out.println("do method2..");
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  private Object lock = new Object();
  public void method3(){    //任何對象鎖
    synchronized (lock) {
      try {
        System.out.println("do method3..");
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  
  public static void main(String[] args) {
    
    final ObjectLock objLock = new ObjectLock();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        objLock.method1();
      }
    });
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        objLock.method2();
      }
    });
    Thread t3 = new Thread(new Runnable() {
      @Override
      public void run() {
        objLock.method3();
      }
    });
    
    t1.start();
    t2.start();
    t3.start();
    
    
  }
  
}      

類鎖和對象鎖不在此過多贅述,任意對象鎖,要實作加鎖,必須是對同一個對象進行操作。雖然是三種類型的鎖的示範,看不出來鎖的優先級,看不出來有任何先後順序,就是簡單的示範。

String鎖的問題

需要注意的是避免使用string類型加鎖。string常量在常量池中隻有一個引用(相當于一把鎖被一直循環持有),是可以實作加鎖的效果的。但是如果new 一個string,就是一個對象,就可以都進來(因為是兩個對象鎖,不互相影響)。

/**
 * synchronized代碼塊對字元串的鎖,注意String常量池的緩存功能
 * @author alienware
 *
 */
public class StringLock {

  public void method() {
    //new String("字元串常量")
    synchronized ("字元串常量") {
      try {
        while(true){
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + "開始");
          Thread.sleep(1000);   
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + "結束");
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  public static void main(String[] args) {
    final StringLock stringLock = new StringLock();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        stringLock.method();
      }
    },"t1");
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        stringLock.method();
      }
    },"t2");
    
    t1.start();
    t2.start();
  }
}      

鎖改變的問題

要注意鎖改變的問題,如果用的字元串變量做的鎖,千萬不要修改這個字元串。如果修改了就起不到加鎖的作用了。

package com.bjsxt.base.sync006;
/**
 * 鎖對象的改變問題
 * @author alienware
 *
 */
public class ChangeLock {

  private String lock = "lock";
  
  private void method(){
    synchronized (lock) {
      try {
        System.out.println("目前線程 : "  + Thread.currentThread().getName() + "開始");
        lock = "change lock";
        Thread.sleep(2000);
        System.out.println("目前線程 : "  + Thread.currentThread().getName() + "結束");
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  
  public static void main(String[] args) {
  
    final ChangeLock changeLock = new ChangeLock();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        changeLock.method();
      }
    },"t1");
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        changeLock.method();
      }
    },"t2");
    t1.start();
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    t2.start();
  }
  
}      

死鎖

package com.bjsxt.base.sync006;

/**
 * 死鎖問題,在設計程式時就應該避免雙方互相持有對方的鎖的情況
 * @author alienware
 *
 */
public class DeadLock implements Runnable{

  private String tag;
  private static Object lock1 = new Object();
  private static Object lock2 = new Object();
  
  public void setTag(String tag){
    this.tag = tag;
  }
  
  @Override
  public void run() {
    if(tag.equals("a")){
      synchronized (lock1) {
        try {
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + " 進入lock1執行");
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        synchronized (lock2) {
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + " 進入lock2執行");
        }
      }
    }
    if(tag.equals("b")){
      synchronized (lock2) {
        try {
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + " 進入lock2執行");
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        synchronized (lock1) {
          System.out.println("目前線程 : "  + Thread.currentThread().getName() + " 進入lock1執行");
        }
      }
    }
  }
  
  public static void main(String[] args) {
    
    DeadLock d1 = new DeadLock();
    d1.setTag("a");
    DeadLock d2 = new DeadLock();
    d2.setTag("b");
     
    Thread t1 = new Thread(d1, "t1");
    Thread t2 = new Thread(d2, "t2");
     
    t1.start();
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    t2.start();
  }
  

  
}