• 详解ThreadLocal与synchronized


    转载自:http://www.java3z.com/cwbwebhome/article/article2/2952.html?id=1648

    文章讲的很详细,全文读完就理解了ThreadLocal和synchronized

    Java良好的支持多线程。使用java,我们可以很轻松的编程一个多线程程序。但是使用多线程可能会引起并发访问的问题。synchronized和ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对ThreadLocal就要陌生得多了。 

          并发问题。当一个对象被两个线程同时访问时,可能有一个线程会得到不可预期的结果。

    一个简单的java类Studnet,代码:
    public class Student { 
      private int age=0; 
      public int getAge() { 
        return this.age; 
      } 

      public void setAge(int age) { 
        this.age = age; 
      }

    }

    一个多线程类ThreadDemo. 
        这个类有一个Student的私有变量,在run方法中,它随机产生一个整数。然后设置到student变量中,从student中读取设置后的值。然后睡眠5秒钟,最后再次读student的age值。

    代码:

    import java.util.*;
    public class ThreadDemo implements Runnable{ 
      Student student = new Student(); 
      public static void main(String[] agrs) { 
        ThreadDemo td = new ThreadDemo(); 
        Thread t1 = new Thread(td,"a"); 
        Thread t2 = new Thread(td,"b"); 
        t1.start(); 
        t2.start(); 
       } 
      public void run() { 
         accessStudent(); 
      } 
      public void accessStudent() { 
       String currentThreadName = Thread.currentThread().getName(); 
       System.out.println(currentThreadName+" is running!"); 
        
         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 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相关的代码块进行同步。

    代码 
    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! 
    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都自加一次, 用来记数线程访问的次数。

    代码 
    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

    我们仍然对student对象以synchronized(this)操作进行同步。 我们需要在两个线程中共享count失败。

    所以仍然需要对count的访问进行同步操作。

    代码 
    long startTime = System.currentTimeMillis(); 
    synchronized(this) { 
      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());   
    }
      long endTime = System.currentTimeMillis(); 
      long spendTime = endTime - startTime; 
      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的访问加锁。

    代码 
    private Object studentLock = new Object(); 
    private Object countLock = new Object(); 

    accessStudent()方法如下: 
    long startTime = System.currentTimeMillis(); 
    String currentThreadName = Thread.currentThread().getName(); 
    System.out.println(currentThreadName+" is running!"); 
    // System.out.println("first read age is:"+this.student.getAge()); 

    synchronized(countLock) { 
    try { 
    this.count++; 
      Thread.sleep(5000); 
    }catch(InterruptedException ex) { 
      ex.printStackTrace(); 

    System.out.println("thread "+currentThreadName+" read count:"+this.count); 
    }

    synchronized(studentLock) { 
      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()); 
       }
      long endTime = System.currentTimeMillis(); 
      long spendTime = endTime - startTime; 
      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来进行的。

    代码 
    protected Student getStudent() { 
      Student student = (Student)studentLocal.get(); 
      if(student == null) { 
       student = new Student(); 
       studentLocal.set(student); 
      } 
      return student; 


    protected void setStudent(Student student) { 
      studentLocal.set(student); 
    }

      accessStudent()方法需要做一些改变。通过调用getStudent()方法来获得当前线程的Student变量,如果当前线程不存在一个Student变量,getStudent方法会创建一个新的Student变量,并设置在当前线程中。 
    Student student = getStudent(); 
    student.setAge(age); 
    accessStudent()方法中无需要任何同步代码。

    完整的代码清单如下: 
    TreadLocalDemo.java

    代码

    import java.util.*;
    public class TreadLocalDemo implements Runnable { 
       private final static ThreadLocal studentLocal = new ThreadLocal(); 
       public static void main(String[] agrs) { 
         TreadLocalDemo td = new TreadLocalDemo(); 
         Thread t1 = new Thread(td,"a"); 
         Thread t2 = new Thread(td,"b"); 
          t1.start(); 
          t2.start(); 
       } 
      public void run() { 
        accessStudent(); 
      } 
      public void accessStudent() { 
        String currentThreadName = Thread.currentThread().getName(); 
        System.out.println(currentThreadName+" is running!"); 
         Random random = new Random(); 
        int age = random.nextInt(100); 
        System.out.println("thread "+currentThreadName +" set age to:"+age); 
        Student student = getStudent(); 
        student.setAge(age); 
        System.out.println("thread "+currentThreadName+" first read age is:"+student.getAge()); 
        try { 
         Thread.sleep(5000); 
        } 
        catch(InterruptedException ex) { 
         ex.printStackTrace(); 
        } 
        System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge()); 
        } 
       protected Student getStudent() { 
         Student student = (Student)studentLocal.get(); 
         if(student == null) { 
           student = new Student(); 
           studentLocal.set(student); 
          } 
          return student; 
        } 
        protected void setStudent(Student student) { 
         studentLocal.set(student); 
        } 
      } 

    运行程序后,屏幕输出: 
    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的一个简单实现

    代码 
    public class ThreadLocal 

      private Map values = Collections.synchronizedMap(new HashMap()); 
      public Object get() 
      { 
       Thread curThread = Thread.currentThread(); 
       Object o = values.get(curThread); 
       if (o == null && !values.containsKey(curThread)) 
       { 
        o = initialValue(); 
        values.put(curThread, o); 
       } 
       return o; 
      } 

      public void set(Object newValue) 
      { 
       values.put(Thread.currentThread(), newValue); 
      } 

      public Object initialValue() 
      { 
       return null; 
      } 
    }

       由此可见,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更加复杂。

    路慢慢其休远羲,吾将上下而求所
  • 相关阅读:
    jbpm4.4+ssh配置(有些使用经验很好)
    ListCell Animation in ListView
    NetBeans IDE 7.4 Beta版本build JavaFX时生成的可执行jar包执行时找不到依赖的jar包
    如何判断Socket连接失效
    log4j:ERROR LogMananger.repositorySelector was null likely due to error in class reloading, using NOPLoggerRepository.
    Jbpm4.4+hibernate3.5.4+spring3.0.4+struts2.1.8整合例子(附完整的请假流程例子,jbpm基础,常见问题解决)
    讲义笔记
    HTML笔记
    JavaScrip——DOM操作(属性操作)
    JavaScrip——DOM操作(查找HTML元素/修改元素)
  • 原文地址:https://www.cnblogs.com/garinzhang/p/2746322.html
Copyright © 2020-2023  润新知