letter Y A N. G Brass Letter F a n-spo D Pewter Uppercase Letter I N G
          隨筆 - 4, 文章 - 10, 評論 - 2, 引用 - 0
          數(shù)據(jù)加載中……

          ThreadLocal與synchronize

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

          一個簡單的java類Studnet 

           1public 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方法中,它隨機產(chǎn)生一個整數(shù)。然后設(shè)置到student變量中,從student中讀取設(shè)置后的值。然后睡眠5秒鐘,最后再次讀student的age值。 

           1public 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值與第二次讀取值不一致。這就是出現(xiàn)了并發(fā)問題。 

          synchronized 
          上面的例子,我們模似了一個并發(fā)問題。Java提供了同步機制來解決并發(fā)問題。synchonzied關(guān)鍵字可以用來同步變量,方法,甚至同步一個代碼塊。 
          使用了同步后,一個線程正在訪問同步對象時,另外一個線程必須等待。 
            Synchronized同步方法 
          現(xiàn)在我們可以對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執(zhí)行完畢后,線程b才開始執(zhí)行。 

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

          Synchronized同步塊 
            在accessStudent方法中,我們真實需要保護的是student變量,所以我們可以進行一個更細粒度的加鎖。我們僅僅對student相關(guān)的代碼塊進行同步。

           1synchronized(this{  
           2Random random = new Random();  
           3int age = random.nextInt(100);  
           4System.out.println("thread "+currentThreadName +" set age to:"+age);  
           5  
           6this.student.setAge(age);  
           7  
           8System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
           9try {  
          10Thread.sleep(5000);  
          11}
            
          12catch(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 

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

          使用同步鎖 
          稍微把上面的例子改一下,在ThreadDemo中有一個私有變量count,。 
             private int count=0; 
          在accessStudent()中, 線程每訪問一次,count都自加一次, 用來記數(shù)線程訪問的次數(shù)。 

          try {  
          this.count++;  
          Thread.sleep(
          5000);  
          }
          catch(InterruptedException ex) {  
              ex.printStackTrace();  
          }
            
          為了模擬線程,所以讓它每次自加后都睡眠5秒。 
          accessStuden()方法的完整代碼如下: 

           String currentThreadName = Thread.currentThread().getName();  
          System.out.println(currentThreadName
          +" is running!");  
            
          try {  
          this.count++;  
          Thread.sleep(
          5000);  
          }
          catch(InterruptedException ex) {  
              ex.printStackTrace();  
          }
            
           System.out.println(
          "thread "+currentThreadName+" read count:"+this.count);  
            
            
          synchronized(this{  
          Random random 
          = new Random();  
          int age = random.nextInt(100);  
          System.out.println(
          "thread "+currentThreadName +" set age to:"+age);  
            
          this.student.setAge(age);  
            
          System.out.println(
          "thread "+currentThreadName+" first  read age is:"+this.student.getAge());  
          try {  
          Thread.sleep(
          5000);  
          }
            
          catch(InterruptedException ex) {  
              ex.printStackTrace();  
          }
            
          }
            
          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 

          我們?nèi)匀粚tudent對象以synchronized(this)操作進行同步。 
          我們需要在兩個線程中共享count失敗。 

          所以仍然需要對count的訪問進行同步操作。 

           1synchronized(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)進行加鎖。有可能會導致太多額外的等待。 
          應該使用不同的對象鎖進行同步。 

          設(shè)置兩個鎖對象,分別用于student和count的訪問加鎖。 

           1private Object studentLock = new Object();  
           2private Object countLock = new Object();  
           3  
           4accessStudent()方法如下:  
           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是實現(xiàn)java的同步機制。同步機制是為了實現(xiàn)同步多線程對相同資源的并發(fā)訪問控制。保證多線程之間的通信。 
          可見,同步的主要目的是保證多線程間的數(shù)據(jù)共享。同步會帶來巨大的性能開銷,所以同步操作應該是細粒度的。如果同步使用得當,帶來的性能開銷是微不足道的。使用同步真正的風險是復雜性和可能破壞資源安全,而不是性能。 


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

          我們把第一個例子用ThreadLocal來實現(xiàn),但是我們需要些許改變。 
          Student并不是一個私有變量了,而是需要封裝在一個ThreadLocal對象中去。調(diào)用ThreadLocal的set方法,ThreadLocal會為每一個線程都保持一份Student變量的副本。所以對student的讀取操作都是通過ThreadLocal來進行的。 

           1protected 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  
          10protected void setStudent(Student student) {  
          11    studentLocal.set(student);  
          12}
            
          accessStudent()方法需要做一些改變。通過調(diào)用getStudent()方法來獲得當前線程的Student變量,如果當前線程不存在一個Student變量,getStudent方法會創(chuàng)建一個新的Student變量,并設(shè)置在當前線程中。 
              Student student = getStudent(); 
              student.setAge(age); 
          accessStudent()方法中無需要任何同步代碼。 

          完整的代碼清單如下: 
          TreadLocalDemo.java 

           1public 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后,我們不需要任何同步代碼,卻能夠保證我們線程間數(shù)據(jù)的安全。 
          而且,ThreadLocal的使用也非常的簡單。 
          我們僅僅需要使用它提供的兩個方法 
          void set(Object obj) 設(shè)置當前線程的變量的副本的值。 
          Object get() 返回當前線程的變量副本 

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

          ThreadLocal是怎么做到為每個線程都維護一個變量的副本的呢? 
          我們可以猜測到ThreadLocal的一個簡單實現(xiàn) 

           

           1public 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是以空間換時間的策略來實現(xiàn)多線程程序。 

          Synchronized還是ThreadLocal? 
          ThreadLocal以空間換取時間,提供了一種非常簡便的多線程實現(xiàn)方式。因為多個線程并發(fā)訪問無需進行等待,所以使用ThreadLocal會獲得更大的性能。雖然使用ThreadLocal會帶來更多的內(nèi)存開銷,但這點開銷是微不足道的。因為保存在ThreadLocal中的對象,通常都是比較小的對象。另外使用ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。 
          ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問。但是ThreadLocal與synchronized有本質(zhì)的區(qū)別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數(shù)據(jù)共享。 
          Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。 
          當然ThreadLocal并不能替代synchronized,它們處理不同的問題域。Synchronized用于實現(xiàn)同步機制,比ThreadLocal更加復雜。

          posted on 2008-10-06 12:13 rainman 閱讀(749) 評論(1)  編輯  收藏 所屬分類: java多線程

          評論

          # re: ThreadLocal與synchronize  回復  更多評論   

          之所以用不同的對象作為鎖,是希望每一個同步塊同時都只能被一個線程所訪問,而不是所有同步塊只能被一個線程所訪問,這是我的理解,不知道是否正確。
          2008-10-06 14:40 | rainman
          主站蜘蛛池模板: 镇平县| 西青区| 乐业县| 太原市| 行唐县| 惠东县| 沙雅县| 平南县| 铜川市| 广灵县| 盘山县| 绥宁县| 阳春市| 萨嘎县| 乾安县| 翁牛特旗| 乌鲁木齐县| 上饶市| 闵行区| 青龙| 应用必备| 祥云县| 宝清县| 襄汾县| 吴旗县| 永嘉县| 玛沁县| 南城县| 温泉县| 那曲县| 柘荣县| 米林县| 台南市| 兰西县| 广元市| 韩城市| 陕西省| 阿拉尔市| 固始县| 万宁市| 祥云县|