免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 834 | 回复: 0
打印 上一主题 下一主题

ThreadLocal与synchronized [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2007-11-07 20:17 |只看该作者 |倒序浏览

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值。
代码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();      }  /* (non-Javadoc)  * @see java.lang.Runnable#run()  */   public void run() {       accessStudent();   }      public void accessStudent() {          String currentThreadName = Thread.currentThread().getName();          System.out.println(currentThreadName+" is running!");         // System.out.println("first  read age is:"+this.student.getAge());          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();  }  }  运行方法后,屏幕输出:
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的访问进行同步操作。
代码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
代码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();                            }           /* (non-Javadoc)      * @see java.lang.Runnable#run()      */      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更加复杂。
               
               
               

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/47086/showart_416929.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP