`
senton
  • 浏览: 205946 次
  • 性别: Icon_minigender_1
  • 来自: 紫禁城
社区版块
存档分类
最新评论

详解ThreadLocal与synchronized

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


      并发问题。

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

一个简单的java类Student,代码:
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更加复杂。

分享到:
评论

相关推荐

    Synchronized与ThreadLocal

    ### Synchronized与ThreadLocal #### 一、Synchronized机制详解 **Synchronized** 是 Java 中一个非常重要的关键字,主要用于实现线程同步。它通过在对象上加锁来确保多个线程能够安全地访问共享资源。 - **作用...

    ThreadLocal详解

    与传统的使用`synchronized`关键字或`Lock`接口来实现线程同步不同,ThreadLocal提供了另一种解决线程安全问题的思路——即为每个线程创建独立的变量副本,避免了线程间的变量共享所带来的同步问题,从而提高了程序...

    8个案例详解教会你ThreadLocal.docx

    - 在面试中,可能会遇到关于 `ThreadLocal` 生命周期管理、内存泄漏、与 `synchronized` 的比较以及在实际应用中的场景分析等问题。 通过以上介绍,我们可以了解到 `ThreadLocal` 在处理多线程环境中提供了独特的...

    2、导致JVM内存泄露的ThreadLocal详解

    ### 导致JVM内存泄露的ThreadLocal详解 #### 一、为什么要有ThreadLocal 在多线程编程中,为了避免线程间的数据竞争和保证线程安全性,常常需要使用同步机制如`synchronized`来控制线程对共享资源的访问。然而,...

    Java 中ThreadLocal类详解

    ThreadLocal并不是用来替代传统的同步机制(如synchronized关键字)的,而是提供了一种不同的并发控制策略。 ThreadLocal类的核心在于它的`set`、`get`和`remove`方法。`set`方法用于设置当前线程的ThreadLocal变量...

    java ThreadLocal使用案例详解

    Java ThreadLocal使用案例详解 Java ThreadLocal是Java语言中的一种机制,用于为每个线程提供一个独立的变量副本,以解决多线程环境下共享变量的线程安全问题。在本文中,我们将详细介绍Java ThreadLocal的使用案例...

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

    为了解决这个问题,我们可以使用线程局部变量(ThreadLocal)或者通过同步机制如synchronized关键字来实现线程间的数据隔离。 1. **线程内的数据共享与对象独立**: 在例子中,使用了HashMap来存储每个线程的数据...

    入研究java.lang.ThreadLocal类.docx

    ### 知识点详解:Java.lang.ThreadLocal 类 #### 一、概述 **ThreadLocal** 并非线程的一种特殊实现形式,而是一种为每个线程提供独立副本的机制,通常被称为“线程局部变量”。这种机制使得每个线程都可以独立...

    java 中ThreadLocal 的正确用法

    在 Java 中,ThreadLocal 实例通常是 private static 字段,位于希望与线程关联状态的类中。例如,在 SerialNum 类中,我们定义了一个私有静态的 ThreadLocal 实例(serialNum),用于维护每个线程的序列号。 ```...

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

    18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题 19.并发容器之BlockingQueue 20.并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解 21.线程池ThreadPoolExecutor实现原理 22.线程池之...

    ==========

    5. **ReentrantLock可重入锁**:`ReentrantLock`是Lock接口的一个实现,具有与`synchronized`相似的功能,但更强大。它支持公平锁和非公平锁,且具备锁的可重入性。 6. **ThreadLocal**:`ThreadLocal`提供线程局部...

    Java并发编程原理与实战

    ThreadLocal 使用及实现原理.mp4 并发工具类CountDownLatch详解.mp4 并发工具类CyclicBarrier 详解.mp4 并发工具类Semaphore详解.mp4 并发工具类Exchanger详解.mp4 CountDownLatch,CyclicBarrier,Semaphore源码解析....

    JAVA多线程设计模式详解

    首先,基础内容介绍涵盖了线程的基本概念,包括进程与线程的区别,线程的创建方式,如通过实现Runnable接口或者继承Thread类,以及线程的状态模型,包括新建、运行、阻塞、等待和终止等状态。此外,还讨论了线程同步...

    java多线程设计模式详解

    这份PDF文档,"java多线程设计模式详解",提供了一种深入理解如何在Java环境中高效利用多线程并保证程序稳定性的途径。下面,我们将详细探讨多线程设计模式的相关知识点。 1. **生产者消费者模式**:这种模式通过...

    java多线程设计模式详解(PDF及源码)

    例如,实现Runnable接口比继承Thread更利于代码的复用,因为它可以与其他类进行组合,避免了Java单继承的限制。 多线程设计模式包括生产者消费者模型、线程池、守护线程、线程同步、死锁预防等。生产者消费者模型是...

    JAVA多线程编程详解-详细操作例子

    此外,Java提供了ThreadLocal类,它为每个线程提供了一个独立的变量副本,解决了线程间共享数据的安全问题,但要注意过度使用可能导致内存泄漏。 总之,理解和掌握Java多线程编程是每个Java开发者必备的技能。正确...

    java多线程编程详解

    在"JAVA多线程编程详解-详细操作例子.doc"和"Java多线程编程详解.doc"文档中,你应该能找到关于以上知识点的具体示例和深入解释,包括如何创建线程、线程间的通信(如wait/notify机制、Semaphore、CountDownLatch)...

    BATJ面试题汇总及详解65页

    - **线程安全实现**:可以通过`synchronized`、`volatile`、`ThreadLocal`、`Atomic`类等机制实现。 2. **JVM相关**: - **JVM内存模型**:包括堆内存、栈内存、方法区、程序计数器、本地方法栈等。 - **GC机制...

    多线程编程详解

    - **线程局部变量**:`ThreadLocal`类,为每个线程创建独立的变量副本,避免线程间的数据干扰。 6. **死锁问题** - **死锁定义**:两个或更多线程相互等待对方释放资源,导致无法继续执行。 - **避免死锁**:...

Global site tag (gtag.js) - Google Analytics