Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 262|回复: 0

[Java线程学习]ThreadLocal与synchronized详解

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-10-30 23:58:30 | 显示全部楼层 |阅读模式
    java 良好的支持多线程。使用java,我们可以很轻松的编程一个多线程程序。但是使用多线程可能会引起并发访问的问题。synchronized和 ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对ThreadLocal就要陌生得多了。  并发问题
       当一个对象被两个线程同时访问时,可能有一个线程会得到不可预期的结果。一个简单的java类Student,代码:
    1. public class Student {
    2. private int age=0;
    3. public int getAge() {
    4.    return this.age;
    5. }
    6. public void setAge(int age) {
    7.    this.age = age;
    8. }
    9. }
    复制代码

      
       
       

         
       

         
       
      

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

    1. import java.util.*;
    2. public class ThreadDemo implements Runnable{
    3.   Student student = new Student();
    4.   public static void main(String[] agrs) {
    5.     ThreadDemo td = new ThreadDemo();
    6.     Thread t1 = new Thread(td,"a");
    7.     Thread t2 = new Thread(td,"b");
    8.     t1.start();
    9.     t2.start();
    10.   }
    11.   public void run() {
    12.     accessStudent();
    13.   }
    14.   public void accessStudent() {
    15.     String currentThreadName = Thread.currentThread().getName();
    16.     System.out.println(currentThreadName+" is running!");

    17.     Random random = new Random();
    18.     int age = random.nextInt(100);
    19.     System.out.println("thread "+currentThreadName +" set age to:"+age);
    20.     this.student.setAge(age);
    21.     System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());

    22.     try {
    23.      Thread.sleep(5000);
    24.     }catch(InterruptedException ex) {
    25.       ex.printStackTrace();
    26.     }
    27.     System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
    28.   }
    29. }
    30. 运行:
    复制代码
    C:java>java ThreadDemo
    a is running!
    thread a set age to:61
    b is running!
    thread b set age to:82
    thread b first read age is:82
    thread a first read age is:61
    thread a second read age is:61
    thread b second read age is:61

    需要注意的是,线程b在同一个方法中,第一次读取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相关的代码块进行同步。
    代码
    1. synchronized(this) {
    2.   Random random = new Random();
    3.   int age = random.nextInt(100);
    4.   System.out.println("thread "+currentThreadName +" set age to:"+age);
    5.   this.student.setAge(age);
    6.   System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
    7.   try {
    8.    Thread.sleep(5000);
    9.   }
    10.   catch(InterruptedException ex) {
    11.     ex.printStackTrace();
    12.   }
    13.    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
    14. }
    复制代码
    运行方法后,屏幕输出:
    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()方法的完整代码如下:
    代码
    1. String currentThreadName = Thread.currentThread().getName();
    2. System.out.println(currentThreadName+" is running!");
    3. try {
    4.   this.count++;
    5.   Thread.sleep(5000);
    6. }catch(InterruptedException ex) {
    7.   ex.printStackTrace();
    8. }
    9. System.out.println("thread "+currentThreadName+" read count:"+this.count);
    10. synchronized(this) {
    11.    Random random = new Random();
    12.    int age = random.nextInt(100);
    13.    System.out.println("thread "+currentThreadName +" set age to:"+age);
    14.    this.student.setAge(age);
    15.    System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
    16.    try {
    17.      Thread.sleep(5000);
    18.    }
    19.    catch(InterruptedException ex) {
    20.     ex.printStackTrace();
    21. }
    22. System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
    23. }
    复制代码
    运行程序后,屏幕输出:
    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的访问进行同步操作。

    1. long startTime = System.currentTimeMillis();
    2.   synchronized(this) {
    3.     try {
    4.      this.count++;
    5.      Thread.sleep(5000);
    6.    }catch(InterruptedException ex) {
    7.      ex.printStackTrace();
    8.   }
    9.     System.out.println("thread "+currentThreadName+" read count:"+this.count);
    10. }
    11.   synchronized(this) {
    12.     Random random = new Random();
    13.     int age = random.nextInt(100);
    14.     System.out.println("thread "+currentThreadName +" set age to:"+age);
    15.     this.student.setAge(age);
    16.    System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
    17.    try {
    18.     Thread.sleep(5000);
    19.    }
    20.   catch(InterruptedException ex) {
    21.     ex.printStackTrace();
    22.    }
    23.     System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
    24.   }
    25.   long endTime = System.currentTimeMillis();
    26.   long spendTime = endTime - startTime;
    27.   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的访问加锁。
    代码  设置两个锁对象,分别用于student和count的访问加锁。
    代码
    1. private Object studentLock = new Object();
    2. private Object countLock = new Object();
    3. accessStudent()方法如下:
    4. long startTime = System.currentTimeMillis();
    5. String currentThreadName = Thread.currentThread().getName();
    6. System.out.println(currentThreadName+" is running!");
    7. // System.out.println("first read age is:"+this.student.getAge());
    8. synchronized(countLock) {
    9. try {
    10.   this.count++;
    11.   Thread.sleep(5000);
    12. }catch(InterruptedException ex) {
    13.    ex.printStackTrace();
    14. }
    15. System.out.println("thread "+currentThreadName+" read count:"+this.count);
    16. }
    17. synchronized(studentLock) {
    18.    Random random = new Random();
    19.    int age = random.nextInt(100);
    20.    System.out.println("thread "+currentThreadName +" set age to:"+age);
    21.    this.student.setAge(age);
    22.    System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
    23.    try {
    24.      Thread.sleep(5000);
    25.    }
    26.     catch(InterruptedException ex) {
    27.       ex.printStackTrace();
    28.    }
    29.    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
    30. }
    31.    long endTime = System.currentTimeMillis();
    32.    long spendTime = endTime - startTime;
    33.    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来进行的。
    1. protected 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. protected void setStudent(Student student) {
    10. studentLocal.set(student);
    11. }

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

    完整的代码清单如下:
    1. import java.util.*;
    2. public class TreadLocalDemo implements Runnable {
    3. private final static ThreadLocal studentLocal = new ThreadLocal();
    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.    t1.start();
    9.    t2.start();
    10. }
    11. public void run() {
    12.    accessStudent();
    13. }
    14. public void accessStudent() {
    15.    String currentThreadName = Thread.currentThread().getName();
    16.    System.out.println(currentThreadName+" is running!");
    17.    Random random = new Random();
    18.    int age = random.nextInt(100);
    19.    System.out.println("thread "+currentThreadName +" set age to:"+age);
    20.    Student student = getStudent();
    21.    student.setAge(age);
    22.    System.out.println("thread "+currentThreadName+" first read age is:"+student.getAge());
    23.    try {
    24.      Thread.sleep(5000);
    25.    }catch(InterruptedException ex) {
    26.      ex.printStackTrace();
    27.    }
    28.    System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge());
    29. }
    30.   protected Student getStudent() {
    31.     Student student = (Student)studentLocal.get();
    32.     if(student == null) {
    33.       student = new Student();
    34.       studentLocal.set(student);
    35.     }
    36.     return student;
    37. }
    38.    protected void setStudent(Student student) {
    39.       studentLocal.set(student);
    40.    }
    41. }
    复制代码

    运行程序后,屏幕输出:
    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的一个简单实现
    代码
    1. public 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.   public void set(Object newValue)
    16.   {
    17.    values.put(Thread.currentThread(), newValue);
    18.   }
    19.   public Object initialValue()
    20.   {
    21.    return null;
    22.   }
    23. }
    复制代码
    由此可见,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更加复杂。


      
      
       
       

         
       

         
       
      
    复制代码

    源码下载:http://file.javaxxz.com/2014/10/30/235830109.zip
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-5 18:38 , Processed in 0.450718 second(s), 46 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表