letter Y A N. G Brass Letter F a n-spo D Pewter Uppercase Letter I N G
随笔 - 4, 文章 - 10, 评论 - 2, 引用 - 0
数据加载中……

ThreadLocal与synchronize

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

一个简单的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方法中,它随机产生一个整数。然后设置到student变量中,从student中读取设置后的值。然后睡眠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值与第二次读取值不一致。这就是出现了并发问题。 

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相关的代码块进行同步。

 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 

需要特别注意这个输出结果。 
这个执行过程比上面的方法同步要快得多了。 
只有对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的访问进行同步操作。 

 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)进行加锁。有可能会导致太多额外的等待。 
应该使用不同的对象锁进行同步。 

设置两个锁对象,分别用于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是实现java的同步机制。同步机制是为了实现同步多线程对相同资源的并发访问控制。保证多线程之间的通信。 
可见,同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,所以同步操作应该是细粒度的。如果同步使用得当,带来的性能开销是微不足道的。使用同步真正的风险是复杂性和可能破坏资源安全,而不是性能。 


ThreadLocal 
由上面可以知道,使用同步是非常复杂的。并且同步会带来性能的降低。Java提供了另外的一种方式,通过ThreadLocal可以很容易的编写多线程程序。从字面上理解,很容易会把ThreadLocal误解为一个线程的本地变量。其它ThreadLocal并不是代表当前线程,ThreadLocal其实是采用哈希表的方式来为每个线程都提供一个变量的副本。从而保证各个线程间数据安全。每个线程的数据不会被另外线程访问和破坏。 

我们把第一个例子用ThreadLocal来实现,但是我们需要些许改变。 
Student并不是一个私有变量了,而是需要封装在一个ThreadLocal对象中去。调用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()方法需要做一些改变。通过调用getStudent()方法来获得当前线程的Student变量,如果当前线程不存在一个Student变量,getStudent方法会创建一个新的Student变量,并设置在当前线程中。 
    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后,我们不需要任何同步代码,却能够保证我们线程间数据的安全。 
而且,ThreadLocal的使用也非常的简单。 
我们仅仅需要使用它提供的两个方法 
void set(Object obj) 设置当前线程的变量的副本的值。 
Object get() 返回当前线程的变量副本 

另外ThreadLocal还有一个protected的initialValue()方法。返回变量副本在当前线程的初始值。默认为null 

ThreadLocal是怎么做到为每个线程都维护一个变量的副本的呢? 
我们可以猜测到ThreadLocal的一个简单实现 

 

 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是以空间换时间的策略来实现多线程程序。 

Synchronized还是ThreadLocal? 
ThreadLocal以空间换取时间,提供了一种非常简便的多线程实现方式。因为多个线程并发访问无需进行等待,所以使用ThreadLocal会获得更大的性能。虽然使用ThreadLocal会带来更多的内存开销,但这点开销是微不足道的。因为保存在ThreadLocal中的对象,通常都是比较小的对象。另外使用ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。 
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。 
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。 
当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

posted on 2008-10-06 12:13 rainman 阅读(714) 评论(1)  编辑  收藏 所属分类: java多线程

评论

# re: ThreadLocal与synchronize  回复  更多评论   

之所以用不同的对象作为锁,是希望每一个同步块同时都只能被一个线程所访问,而不是所有同步块只能被一个线程所访问,这是我的理解,不知道是否正确。
2008-10-06 14:40 | rainman

只有注册用户登录后才能发表评论。


网站导航: