ThreadLocal與synchronized


ThreadLocal與synchronized 
Java良好的支持多線程。使用java,我們可以很輕鬆的編程一個多線程程序。但是使用多線程可能會引起併發訪問的問題。synchronized和ThreadLocal都是用來解決多線程併發訪問的問題。大家可能對synchronized較爲熟悉,而對ThreadLocal就要陌生得多了。 
併發問題。當一個對象被兩個線程同時訪問時,可能有一個線程會得到不可預期的結果。 

一個簡單的java類Studnet 
Java代碼  收藏代碼
  1. public class Student {  
  2.   private int age=0;  
  3.     
  4.   public int getAge() {  
  5.       return this.age;  
  6.         
  7.   }  
  8.     
  9.   public void setAge(int age) {  
  10.       this.age = age;  
  11.   }  
  12. }  

一個多線程類ThreadDemo. 
這個類有一個Student的私有變量,在run方法中,它隨機產生一個整數。然後設置到student變量中,從student中讀取設置後的值。然後睡眠5秒鐘,最後再次讀student的age值。 

Java代碼  收藏代碼
  1. public class ThreadDemo implements Runnable{  
  2.   Student student = new Student();  
  3.   public static void main(String[] agrs) {  
  4.      ThreadDemo td = new ThreadDemo();  
  5.      Thread t1 = new Thread(td,"a");  
  6.      Thread t2 = new Thread(td,"b");  
  7.     t1.start();  
  8.     t2.start();  
  9.   
  10.   }  
  11. /* (non-Javadoc) 
  12.  * @see java.lang.Runnable#run() 
  13.  */  
  14.  public void run() {  
  15.      accessStudent();  
  16.  }  
  17.    
  18.  public void accessStudent() {  
  19.         String currentThreadName = Thread.currentThread().getName();  
  20.         System.out.println(currentThreadName+" is running!");  
  21.        // System.out.println("first  read age is:"+this.student.getAge());  
  22.         Random random = new Random();  
  23.         int age = random.nextInt(100);  
  24.         System.out.println("thread "+currentThreadName +" set age to:"+age);  
  25.          
  26.         this.student.setAge(age);  
  27.         System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
  28.         try {  
  29.         Thread.sleep(5000);  
  30.         }  
  31.         catch(InterruptedException ex) {  
  32.             ex.printStackTrace();  
  33.         }  
  34.         System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());  
  35.            
  36.  }  
  37.     
  38. }  
運行這個程序,屏幕輸出如下: 
a is running! 
b is running! 
thread b set age to:33 
thread b first  read age is:33 
thread a set age to:81 
thread a first  read age is:81 
thread b second read age is:81 
thread a second read age is:81 

需要注意的是,線程a在同一個方法中,第一次讀取student的age值與第二次讀取值不一致。這就是出現了併發問題。 

synchronized 
上面的例子,我們模似了一個併發問題。Java提供了同步機制來解決併發問題。synchonzied關鍵字可以用來同步變量,方法,甚至同步一個代碼塊。 
使用了同步後,一個線程正在訪問同步對象時,另外一個線程必須等待。 
  Synchronized同步方法 
現在我們可以對accessStudent方法實施同步。 
public synchronized void  accessStudent() 
再次運行程序,屏幕輸出如下: 
a is running! 
thread a set age to:49 
thread a first  read age is:49 
thread a second read age is:49 
b is running! 
thread b set age to:17 
thread b first  read age is:17 
thread b second read age is:17 

加上了同步後,線程b必須等待線程a執行完畢後,線程b纔開始執行。 

對方法進行同步的代價是非常昂貴的。特別是當被同步的方法執行一個冗長的操作。這個方法執行會花費很長的時間,對這樣的方法進行同步可能會使系統性能成數量級的下降。 

Synchronized同步塊 
  在accessStudent方法中,我們真實需要保護的是student變量,所以我們可以進行一個更細粒度的加鎖。我們僅僅對student相關的代碼塊進行同步。 
Java代碼  收藏代碼
  1. synchronized(this) {  
  2. Random random = new Random();  
  3. int age = random.nextInt(100);  
  4. System.out.println("thread "+currentThreadName +" set age to:"+age);  
  5.   
  6. this.student.setAge(age);  
  7.   
  8. System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
  9. try {  
  10. Thread.sleep(5000);  
  11. }  
  12. catch(InterruptedException ex) {  
  13.     ex.printStackTrace();  
  14. }  
  15. }  
運行方法後,屏幕輸出: 
a is running! 
thread a set age to:18 
thread a first  read age is:18 
b is running! 
thread a second read age is:18 
thread b set age to:62 
thread b first  read age is:62 
thread b second read age is:62 

需要特別注意這個輸出結果。 
這個執行過程比上面的方法同步要快得多了。 
只有對student進行訪問的代碼是同步的,而其它與部份代碼卻是異步的了。而student的值並沒有被錯誤的修改。如果是在一個真實的系統中,accessStudent方法的操作又比較耗時的情況下。使用同步的速度幾乎與沒有同步一樣快。 

使用同步鎖 
稍微把上面的例子改一下,在ThreadDemo中有一個私有變量count,。 
   private int count=0; 
在accessStudent()中, 線程每訪問一次,count都自加一次, 用來記數線程訪問的次數。 
Java代碼  收藏代碼
  1. try {  
  2. this.count++;  
  3. Thread.sleep(5000);  
  4. }catch(InterruptedException ex) {  
  5.     ex.printStackTrace();  
  6. }  
  爲了模擬線程,所以讓它每次自加後都睡眠5秒。 
accessStuden()方法的完整代碼如下: 
Java代碼  收藏代碼
  1.    String currentThreadName = Thread.currentThread().getName();  
  2. System.out.println(currentThreadName+" is running!");  
  3.   try {  
  4. this.count++;  
  5. Thread.sleep(5000);  
  6. }catch(InterruptedException ex) {  
  7.     ex.printStackTrace();  
  8. }  
  9.  System.out.println("thread "+currentThreadName+" read count:"+this.count);  
  10.   
  11.   
  12. synchronized(this) {  
  13. Random random = new Random();  
  14. int age = random.nextInt(100);  
  15. System.out.println("thread "+currentThreadName +" set age to:"+age);  
  16.   
  17. this.student.setAge(age);  
  18.   
  19. System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
  20. try {  
  21. Thread.sleep(5000);  
  22. }  
  23. catch(InterruptedException ex) {  
  24.     ex.printStackTrace();  
  25. }  
  26. }  
  27. System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());  
    運行程序後,屏幕輸出: 
a is running! 
b is running! 
thread a read count:2 
thread a set age to:49 
thread a first  read age is:49 
thread b read count:2 
thread a second read age is:49 
thread b set age to:7 
thread b first  read age is:7 
thread b second read age is:7 

我們仍然對student對象以synchronized(this)操作進行同步。 
我們需要在兩個線程中共享count失敗。 

所以仍然需要對count的訪問進行同步操作。 
Java代碼  收藏代碼
  1. synchronized(this) {  
  2.   try {  
  3.   this.count++;  
  4.   Thread.sleep(5000);  
  5.   }catch(InterruptedException ex) {  
  6.     ex.printStackTrace();  
  7.   }  
  8.   }  
  9.   System.out.println("thread "+currentThreadName+" read count:"+this.count);  
  10.     
  11.    
  12.   synchronized(this) {  
  13.   Random random = new Random();  
  14.   int age = random.nextInt(100);  
  15.   System.out.println("thread "+currentThreadName +" set age to:"+age);  
  16.    
  17.   this.student.setAge(age);  
  18.    
  19.   System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
  20.   try {  
  21.   Thread.sleep(5000);  
  22.   }  
  23.   catch(InterruptedException ex) {  
  24.     ex.printStackTrace();  
  25.   }  
  26.   }  
  27.   System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());  
  28.   long endTime = System.currentTimeMillis();  
  29.   long spendTime = endTime - startTime;  
  30.   System.out.println("花費時間:"+spendTime +"毫秒");  

程序運行後,屏幕輸出 
a is running! 
b is running! 
thread a read count:1 
thread a set age to:97 
thread a first  read age is:97 
thread a second read age is:97 
花費時間:10015毫秒 
thread b read count:2 
thread b set age to:47 
thread b first  read age is:47 
thread b second read age is:47 
花費時間:20124毫秒 

我們在同一個方法中,多次使用synchronized(this)進行加鎖。有可能會導致太多額外的等待。 
應該使用不同的對象鎖進行同步。 

設置兩個鎖對象,分別用於student和count的訪問加鎖。 
Java代碼  收藏代碼
  1.  private Object studentLock = new Object();  
  2. private Object countLock = new Object();  
  3.   
  4. accessStudent()方法如下:  
  5.      long startTime = System.currentTimeMillis();  
  6.         String currentThreadName = Thread.currentThread().getName();  
  7.         System.out.println(currentThreadName+" is running!");  
  8.        // System.out.println("first  read age is:"+this.student.getAge());  
  9.   
  10.          synchronized(countLock) {  
  11.         try {  
  12.         this.count++;  
  13.         Thread.sleep(5000);  
  14.         }catch(InterruptedException ex) {  
  15.             ex.printStackTrace();  
  16.         }  
  17.         }  
  18.         System.out.println("thread "+currentThreadName+" read count:"+this.count);  
  19.           
  20.          
  21.         synchronized(studentLock) {  
  22.         Random random = new Random();  
  23.         int age = random.nextInt(100);  
  24.         System.out.println("thread "+currentThreadName +" set age to:"+age);  
  25.          
  26.         this.student.setAge(age);  
  27.          
  28.         System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
  29.         try {  
  30.         Thread.sleep(5000);  
  31.         }  
  32.         catch(InterruptedException ex) {  
  33.             ex.printStackTrace();  
  34.         }  
  35.         }  
  36.         System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());  
  37.         long endTime = System.currentTimeMillis();  
  38.         long spendTime = endTime - startTime;  
  39.         System.out.println("花費時間:"+spendTime +"毫秒");  

這樣對count和student加上了兩把不同的鎖。 

運行程序後,屏幕輸出: 
a is running! 
b is running! 
thread a read count:1 
thread a set age to:48 
thread a first  read age is:48 
thread a second read age is:48 
花費時間:10016毫秒 
thread b read count:2 
thread b set age to:68 
thread b first  read age is:68 
thread b second read age is:68 
花費時間:20046毫秒 
與兩次使用synchronized(this)相比,使用不同的對象鎖,在性能上可以得到更大的提升。 

由此可見synchronized是實現java的同步機制。同步機制是爲了實現同步多線程對相同資源的併發訪問控制。保證多線程之間的通信。 
可見,同步的主要目的是保證多線程間的數據共享。同步會帶來巨大的性能開銷,所以同步操作應該是細粒度的。如果同步使用得當,帶來的性能開銷是微不足道的。使用同步真正的風險是複雜性和可能破壞資源安全,而不是性能。 


ThreadLocal 
由上面可以知道,使用同步是非常複雜的。並且同步會帶來性能的降低。Java提供了另外的一種方式,通過ThreadLocal可以很容易的編寫多線程程序。從字面上理解,很容易會把ThreadLocal誤解爲一個線程的本地變量。其它ThreadLocal並不是代表當前線程,ThreadLocal其實是採用哈希表的方式來爲每個線程都提供一個變量的副本。從而保證各個線程間數據安全。每個線程的數據不會被另外線程訪問和破壞。 

我們把第一個例子用ThreadLocal來實現,但是我們需要些許改變。 
Student並不是一個私有變量了,而是需要封裝在一個ThreadLocal對象中去。調用ThreadLocal的set方法,ThreadLocal會爲每一個線程都保持一份Student變量的副本。所以對student的讀取操作都是通過ThreadLocal來進行的。 
Java代碼  收藏代碼
  1. protected Student getStudent() {  
  2.     Student student = (Student)studentLocal.get();  
  3.     if(student == null) {  
  4.         student = new Student();  
  5.         studentLocal.set(student);  
  6.     }  
  7.     return student;  
  8. }  
  9.   
  10. protected void setStudent(Student student) {  
  11.     studentLocal.set(student);  
  12. }  

accessStudent()方法需要做一些改變。通過調用getStudent()方法來獲得當前線程的Student變量,如果當前線程不存在一個Student變量,getStudent方法會創建一個新的Student變量,並設置在當前線程中。 
    Student student = getStudent(); 
    student.setAge(age); 
accessStudent()方法中無需要任何同步代碼。 

完整的代碼清單如下: 
TreadLocalDemo.java 
Java代碼  收藏代碼
  1. public class TreadLocalDemo implements Runnable {  
  2.    private final static  ThreadLocal studentLocal = new ThreadLocal();  
  3.      
  4.    public static void main(String[] agrs) {  
  5.        TreadLocalDemo td = new TreadLocalDemo();  
  6.          Thread t1 = new Thread(td,"a");  
  7.          Thread t2 = new Thread(td,"b");  
  8.           
  9.         t1.start();  
  10.         t2.start();  
  11.          
  12.          
  13.   
  14.   
  15.       }  
  16.      
  17.     /* (non-Javadoc) 
  18.      * @see java.lang.Runnable#run() 
  19.      */  
  20.     public void run() {  
  21.          accessStudent();  
  22.     }  
  23.   
  24.     public  void  accessStudent() {  
  25.           
  26.         String currentThreadName = Thread.currentThread().getName();  
  27.         System.out.println(currentThreadName+" is running!");  
  28.         Random random = new Random();  
  29.         int age = random.nextInt(100);  
  30.         System.out.println("thread "+currentThreadName +" set age to:"+age);  
  31.         Student student = getStudent();  
  32.         student.setAge(age);  
  33.         System.out.println("thread "+currentThreadName+" first  read age is:"+student.getAge());  
  34.         try {  
  35.         Thread.sleep(5000);  
  36.         }  
  37.         catch(InterruptedException ex) {  
  38.             ex.printStackTrace();  
  39.         }  
  40.         System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge());  
  41.           
  42.     }  
  43.       
  44.     protected Student getStudent() {  
  45.         Student student = (Student)studentLocal.get();  
  46.         if(student == null) {  
  47.             student = new Student();  
  48.             studentLocal.set(student);  
  49.         }  
  50.         return student;  
  51.     }  
  52.       
  53.     protected void setStudent(Student student) {  
  54.         studentLocal.set(student);  
  55.     }  
  56. }  
運行程序後,屏幕輸出: 
b is running! 
thread b set age to:0 
thread b first  read age is:0 
a is running! 
thread a set age to:17 
thread a first  read age is:17 
thread b second read age is:0 
thread a second read age is:17 

可見,使用ThreadLocal後,我們不需要任何同步代碼,卻能夠保證我們線程間數據的安全。 
而且,ThreadLocal的使用也非常的簡單。 
我們僅僅需要使用它提供的兩個方法 
void set(Object obj) 設置當前線程的變量的副本的值。 
Object get() 返回當前線程的變量副本 

另外ThreadLocal還有一個protected的initialValue()方法。返回變量副本在當前線程的初始值。默認爲null 

ThreadLocal是怎麼做到爲每個線程都維護一個變量的副本的呢? 
我們可以猜測到ThreadLocal的一個簡單實現 
Java代碼  收藏代碼
  1. public class ThreadLocal  
  2. {  
  3.  private Map values = Collections.synchronizedMap(new HashMap());  
  4.  public Object get()  
  5.  {  
  6.   Thread curThread = Thread.currentThread();   
  7.   Object o = values.get(curThread);   
  8.   if (o == null && !values.containsKey(curThread))  
  9.   {  
  10.    o = initialValue();  
  11.    values.put(curThread, o);   
  12.   }  
  13.   return o;   
  14.  }  
  15.   
  16.  public void set(Object newValue)  
  17.  {  
  18.   values.put(Thread.currentThread(), newValue);  
  19.  }  
  20.   
  21.  public Object initialValue()  
  22.  {  
  23.   return null;   
  24.  }  
  25. }  

由此可見,ThreadLocal通過一個Map來爲每個線程都持有一個變量副本。這個map以當前線程爲key。與synchronized相比,ThreadLocal是以空間換時間的策略來實現多線程程序。 

Synchronized還是ThreadLocal? 
ThreadLocal以空間換取時間,提供了一種非常簡便的多線程實現方式。因爲多個線程併發訪問無需進行等待,所以使用ThreadLocal會獲得更大的性能。雖然使用ThreadLocal會帶來更多的內存開銷,但這點開銷是微不足道的。因爲保存在ThreadLocal中的對象,通常都是比較小的對象。另外使用ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。 
ThreadLocal和Synchonized都用於解決多線程併發訪問。但是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal爲每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信時能夠獲得數據共享。 
Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。 
當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加複雜。 
發佈了164 篇原創文章 · 獲贊 26 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章