`

ThreadLocal与synchronized多线程并发访问区别【转】

阅读更多
[from http://hi.baidu.com/lffsonic/blog/item/4c03b3fba624f4839e51469d.html]
Java良好的支持多线程。使用java,我们可以很轻松的编程一个多线程程序。但是使用多线程可能会引起并发访问的问题。synchronized和ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对ThreadLocal就要陌生得多了。
并发问题。当一个对象被两个线程同时访问时,可能有一个线程会得到不可预期的结果。

一个简单的java类Studnet
Java代码
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值。

Java代码
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相关的代码块进行同步。
Java代码
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都自加一次, 用来记数线程访问的次数。
Java代码
try {  
this.count++;  
Thread.sleep(5000);  
}catch(InterruptedException ex) {  
     ex.printStackTrace();  

为了模拟线程,所以让它每次自加后都睡眠5秒。
accessStuden()方法的完整代码如下:

Java代码
    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的访问进行同步操作。
Java代码
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的访问加锁。
Java代码
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来进行的。

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

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的一个简单实现

Java代码
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更加复杂。
分享到:
评论

相关推荐

    Synchronized与ThreadLocal

    - 在多线程环境中,当多个线程尝试访问同一份资源时,可以使用 synchronized 来保证同一时刻只有一个线程能执行特定的代码段。 - 可用于防止数据竞争条件(race conditions),例如在更新共享变量时确保一致性。 ...

    Java多线程并发访问解决方案

    在Java编程中,多线程并发访问是提升应用程序性能的重要手段,但同时也带来了数据一致性、安全性及效率等问题。本文将深入探讨Java中的多线程并发访问解决方案,主要围绕以下几个核心知识点进行阐述: 1. **线程...

    JDK5中的多线程并发库.doc

    在JDK5中,Java引入了一套强大的多线程并发库,极大地提升了多线程编程的效率和安全性。这个库提供了丰富的类和接口,使得开发者能够更好地控制线程的执行、同步和通信。 1. **线程** - **线程与进程的关系**:...

    Java多线程 之 临界区、ThreadLocal.docx

    Java多线程编程中,临界区和ThreadLocal是两种重要的并发控制机制,它们用于解决多线程环境下的数据安全问题。 1. **临界区(Critical Section)** 临界区是指一段代码,它在同一时刻只允许一个线程进行访问。在...

    JDK5中的多线程并发库

    在JDK5中,多线程并发库引入了一系列新的特性,极大地增强了Java处理并发问题的能力。以下是关于这个主题的详细解释: 1. **线程**: - **线程与进程的关系**:进程是一个正在执行的程序实体,而线程是进程内部的...

    Java 多线程与并发编程总结.doc

    Java多线程与并发编程是Java开发中不可或缺的一部分,它涉及到如何高效地利用CPU资源,实现并发执行多个任务。在操作系统层面,多线程是为了提高系统利用率,使得多个任务能够"同时"执行,但实际上,由于CPU的时钟...

    【2018最新最详细】并发多线程教程

    【2018最新最详细】并发多线程教程,课程结构如下 1.并发编程的优缺点 2.线程的状态转换以及基本操作 3.java内存模型以及happens-before规则 4.彻底理解synchronized 5.彻底理解volatile 6.你以为你真的了解final吗...

    张孝祥Java多线程与并发库高级应用笔记

    ### 张孝祥Java多线程与并发库高级应用笔记概览 #### 一、Java多线程技术的重要性与挑战 Java线程技术是软件工程领域不可或缺的一部分,尤其在底层编程、Android应用开发以及游戏开发中,其重要性不言而喻。然而,...

    ThreadLocal

    ThreadLocal是Java编程语言中的一个类,用于在多线程环境中提供线程局部变量。它是一种特殊类型的变量,每个线程都有自己的副本,互不影响,从而实现线程间数据隔离。ThreadLocal通常被用来解决线程共享数据时可能...

    经典Java多线程与并发库高级应用

    在深入探讨Java多线程与并发库的高级应用前,有必要了解一些基础概念。Java线程是Java程序的基础,它代表程序中的一条执行线索或线路。在Java中创建线程有两种传统方式,一种是通过继承Thread类并覆盖其run方法来...

    对java的BitSet的多线程并发的探索

    在多线程并发环境中,对BitSet的操作需要特别注意,因为位操作本身是原子性的,但BitSet的大部分方法并不是线程安全的。这篇博文主要探讨了如何在多线程环境下正确地使用Java的BitSet。 首先,我们要理解BitSet的...

    Java多线程并发机制的应用探讨.zip

    Java多线程并发机制是Java编程中的重要组成部分,它允许程序在多个线程间同时执行,从而提升系统性能和响应速度。并发编程是现代软件设计中的基石,尤其是在服务器端应用和分布式系统中,多线程技术是实现高效能、高...

    Java多线程与并发库视频资源网盘链接

    Java多线程与并发库是Java开发者必备的重要技能之一,尤其对于处理高并发场景的应用开发至关重要。本视频资源集合旨在帮助初级开发人员深入理解和掌握这一关键领域,通过学习,你可以提升自己的程序性能优化能力,更...

    JAVA高质量并发详解,多线程并发深入讲解

    - **线程安全问题:** 如何避免共享资源访问冲突,确保多线程环境下的数据一致性。 - **核心API:** - **synchronized关键字:** 实现对象或代码块级别的独占锁,用于保证线程安全。 - **Lock接口:** 更灵活的...

    java多线程设计

    不可变对象在多线程环境中具有天然的安全性,因为它们的值不会在并发访问时被意外修改。这使得多个线程可以共享一个不可变对象,无需担心数据一致性问题,从而提高了程序的并发性能。 三、Java多线程防止非安全问题...

    多线程面试题

    11. **线程安全**:线程安全的类或方法可以在多线程环境中被正确使用,不会因为并发访问而导致数据的不一致。Java提供了一些线程安全的集合类,如Vector、ConcurrentHashMap等。 12. **线程局部变量**:ThreadLocal...

    多线程相关代码(V3)

    本资源包含的"多线程相关代码(V3)"提供了关于多线程编程的一些示例,涵盖了Lock、synchronized、Join、ThreadLocal、Executors以及CountDownLatch等多个关键概念。下面我们将详细探讨这些知识点。 1. **Lock接口*...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    在Java编程领域,多线程是一项至关重要的技术,它能够充分利用多核处理器的计算能力,提高应用程序的响应速度和并发性能。《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为...

    java核心知识点学习----多线程间的数据共享和对象独立,ThreadLocal详解.pdf

    Java中的多线程编程是开发高并发应用的关键技术之一,涉及到如何有效管理和利用系统资源,尤其是在处理并发数据访问时,确保数据的安全性和一致性至关重要。在Java中,有多种方式可以实现线程间的数据共享和对象独立...

    Java多线程知识,龙果学院

    10. **CountDownLatch、CyclicBarrier、Semaphore等并发工具类**:这些工具可以帮助我们在多线程环境中进行同步,例如计数器、等待所有线程完成的栅栏以及限制并发访问的信号量。 四、并发编程高级特性 11. **...

Global site tag (gtag.js) - Google Analytics