一文了解java并发工具----------ThreadLocal

声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!

文章目录

一、ThreadLocal简介

1、什么是ThreadLocal?

我们知道,在多线程程序中,多个线程访问同一个共享变量,很容易造成线程不安全,出现并发问题。特别是多个线程对同一个共享变量进行写入的时候,为了保证线程安全,一般我们都会在访问共享变量的时候需要进行一些额外的同步措施才能保证线程安全。

ThreadLocal是JDK包提供的,它是一个并发工具,它是除了加锁这种同步方式之外的一种保证、一种规避多线程访问出现线程不安全的方法,因为它每个线程对其进行访问的时候访问的都是线程内自己的变量,所以这样就不会存在线程不安全问题。

二、两大应用场景

1、每个线程需要一个独立的对象

  • 每个Thread内有自己的实例副本,线程之间不共享

这是什么意思呢? 举个简单的例子,假如现在只有一个本教材,而使用的学生很多,如果一起在书本上写笔记,那么就会产生线程安全,但是如果我们每个学生都去复印这本教材,然后自己在自己的那本教材上做笔记,那么这样就岂不是避免了多个人使用同一本而产生的线程安全。

Threadlocal就是这样。接下来用代码展示一个例子

代码展示

用多个线程访问同一个工具类(工具类一般都是线程不安全的),因为是多个线程,为了避免每个线程因创建和销毁而产生开销,这里使用了线程池。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author delingw
 * @version 1.0
 */
public class FormatTime {
    // 创建线程池
    public static ExecutorService service = Executors.newFixedThreadPool(10);

    // 工具类
    public String date(int secends) {
        Date date = new Date(1000 * secends);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            service.submit(new Runnable() {
                @Override
                public void run() {
                    String s = new FormatTime().date(finalI);
                    System.out.println(s);
                }
            });
        }
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

分析: 不难看出每次创建一个线程,就会创建一个SimpleDateFormat实例,这样会太消耗内存,然后聪明的小伙伴就会说,那还不简单,直接弄成共享的呗。这样是不行的,因为这样就造成线程不安全。

代码展示

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author delingw
 * @version 1.0
 */
public class FormatTime {
    // 创建线程池
    public static ExecutorService service = Executors.newFixedThreadPool(10);
    public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    // 工具类
    public String date(int secends) {
        Date date = new Date(1000 * secends);
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            service.submit(new Runnable() {
                @Override
                public void run() {
                    String s = new FormatTime().date(finalI);
                    System.out.println(s);
                }
            });
        }
        service.shutdown();
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

通过上述运行结果,可以看出已经出现了线程不安全,然后聪明的小伙伴则又说,这还不简单,出现了线程安全,那我直接加个锁呗,那不就保证了线程的安全了,

其实这样如果不考虑性能问题的话,这一想法是行的通,但是这样会有很大的性能开销,为什么呢?因为如果加锁,就表明,每个线程必须等待上一个线程释放了锁,才能拿到锁,这样就会造成很大的性能开销。(这里况且还是多个线程呢)

代码展示

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author delingw
 * @version 1.0
 */
public class FormatTime {
    // 创建线程池
    public static ExecutorService service = Executors.newFixedThreadPool(10);
    public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    // 工具类
    public String date(int secends) {
        Date date = new Date(1000 * secends);
        String s = null;
        synchronized (FormatTime.class) {
            s = dateFormat.format(date);
        }
        return s;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            service.submit(new Runnable() {
                @Override
                public void run() {
                    String s = new FormatTime().date(finalI);
                    System.out.println(s);
                }
            });
        }
        service.shutdown();
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

为了解决这个性能问题,这里就要用我们的ThreadLocal了。既保证了线程安全,也保证了性能。

代码演示

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author delingw
 * @version 1.0
 */
public class FormatTime {
	// 线程池
    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    String s = new FormatTime().date(finalI);
                    System.out.println(s);
                }
            });
        }
        executorService.shutdown();
    }
    // 工具类
    public String date(int secends) {
        Date date = new Date(1000 * secends);
        // 获取值
        SimpleDateFormat format = ThreadlocalClass.threadLocal1.get();
        return format.format(date);
    }
}
/**
 * 两种方式都可以
 */
class ThreadlocalClass {
    // 匿名类
    public static ThreadLocal<SimpleDateFormat> threadLocal1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
    //Lambda表达式
    public static ThreadLocal<SimpleDateFormat> threadLocal2 = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
    );

}

2、每个线程内需要保存全局变量

  • 每个线程内需要保存全局变量

举个简单的例子,例如我们在写业务的时候,我们需要在拦截器中获取用户的信息(一个请求就是一个线程,一个线程就是一个用户,用户之间的用户信息是不同的)。让当前用户信息需要被线程内所有方法共享。

代码展示

/**
 * @author delingw
 * @version 1.0
 */
public class ThreadLocalUserInfo {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("张三");
        ThreadLocalHodler.userThreadLocal.set(user);
        System.out.println("service1拿到了 "+user.name);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = ThreadLocalHodler.userThreadLocal.get();
        System.out.println("service2拿到了 "+user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = ThreadLocalHodler.userThreadLocal.get();
        System.out.println("service3拿到了 "+user.name);
    }
}


class ThreadLocalHodler {
    // 用户信息ThreadLocal
    public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
}


// 用户信息
class User {
    public String name;

    public User(String name) {
        this.name = name;
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

3、两种应用场景的总结

  • 场景一  initialValue

在ThreadLocal第一次get的时候把对象给初始化出来(延迟加载),对象的初始化由我们控制。

  • 场景二  set

保存到ThreadLocal里的对象不由我们控制,例如拦截器生成的用户信息,我们就可以用ThreadLocal.set()直接放到我们的ThreadLocal中去,以便我们后续使用

两种应用场景源码分析

通过源码可以看出,不管是先setInitialValue还是直接set,最后都是利用map.set()方法来设置值
也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样

一文了解java并发工具----------ThreadLocal
一文了解java并发工具----------ThreadLocal

三、使用ThreadLocal的好处

  • 达到线程安全

每个线程都有自己独立的实例,自己访问自己的。

  • 不需要加锁,提高执行效率

不需要加锁,就省去了等待拿锁的开销

  • 更高效地利用内存、节省开销

例如场景一需要每次都要创建一个实例

  • 免去传参的繁琐

不管是场景一还是场景二,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次传同样的参数。ThreadLocal使得代码耦合度更低,更优雅。

四、实现原理、源码分析

1、ThreadLocal实现线程独立的原理

通过源码可得,每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal引用作为key,从Map集合中查找value值。这就是ThreadLocal实现线程独立的原理。也就是说,ThreadLocal能够做到线程独立,是因为值并不存在ThreadLocal中,而是存储在线程对象中。

源码分析

  • ThreadLocalMap类 也就是 Thread.threadLocals

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map键值对。

  • 键:  Threadlocal引用
  • 值: 与键相对应的ThreadLocal值。

此ThreadLocalMap解决冲突的方法:

采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链。

一文了解java并发工具----------ThreadLocal
一文了解java并发工具----------ThreadLocal

2、常用方法源码解析

  • initialValue()

源码展示

1. 该方法会返回当前线程相对应的"初始值",这是一个延迟加载的方法,只有调用get()的时候,才会触发。默认返回null

一文了解java并发工具----------ThreadLocal

2. 当线程第一次使用get()方法访问变量时,将会调用该方法,除非线程先调用了set()方法,在这种情况下,不会为线程调用本initialValue()方法。

一文了解java并发工具----------ThreadLocal
一文了解java并发工具----------ThreadLocal

3. 通常,每个线程最多调用一次此方法,但如果已经调用了remove(),则可以再次调用此方法。

4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写此方法。以便在后续使用中可以初始化副本对象。
一文了解java并发工具----------ThreadLocal

  • set()

为这个线程设置一个新值

源码展示
一文了解java并发工具----------ThreadLocal

  • get()

获取这个线程相对应的value

如果是首次调用此方法,则会调用initialValue来得到这个值。

源码展示
一文了解java并发工具----------ThreadLocal

  • remove()

删除对应这个线程的值

注意:这里只是删除的是单个Entry对象,而不是整个ThreadLocalMap中的值。

源码展示
一文了解java并发工具----------ThreadLocal
一文了解java并发工具----------ThreadLocal

五、ThreadLocal使用不当,出现的内存泄露以及避免方案

1、什么是内存泄露?

内存泄露指的是某个对象不再有用,但是占用的内存却不能回收。

2、使用不当,出现内存泄露

  • 弱引用的特点

如果这个对象只被弱引用关联,没有任何强引用关联的话,那么这个对象就会被GC回收

出现内存泄露的原因

出现内存泄露原因是ThreadLocalMap中Entrykey是弱引用,而value是强引用。

可以从源码看出,ThreadLocalMap中的Entry 继承自WeakReference弱引用,并且每个Entry都是对key弱引用,但对value却是强引用。

正常情况下、当线程终止,保存在ThreadLocal里的value会被垃圾回收。但是如果线程不终止(例如线程池),就可能出现key为null,而value不为null的这样的情况。正是因为这种情况,所以会导致value不能被回收。所以就可能会出现OOM

一文了解java并发工具----------ThreadLocal
解决方案

JDK已经考虑了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry ,并把相对应的value设置为null,这样value对象就会被回收。

但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么还是会造成OOM

如何避免内存泄露(阿里规约)

调用remove()方法,就会删除对应的Entry对象,可以避免内存泄露,所有使用完ThreadLocal之后,应该调用remove()方法

六、ThreadLocal注意点

  • 空指针异常 在进行get之前,必须先set,不然就会出现空指针异常?

如果在调用get之前,没有先调用set,那么它就会返回null

出现空指针异常的原因是因为可能我们在拆箱的过程中,而出现的NullPointerException异常

代码展示

/**
 * @author delingw
 * @version 1.0
 */
public class Demo {
    ThreadLocal<Long> local = new ThreadLocal<>();

    public void set() {
        local.set(10L);
    }
	
	//  ThreadLocal中是Long,而因拆箱的原因导致了空指针异常
    public long get() {
        return local.get();
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        long l = demo.get();
        System.out.println(l);
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

正确代码展示

/**
 * @author delingw
 * @version 1.0
 */
public class Demo {
    static ThreadLocal<Long> local = new ThreadLocal<>();

    public void set() {
        local.set(10L);
    }

    public Long get() {
        return local.get();
    }

    public static void main(String[] args) {
    	// 直接调用get
        Long l = local.get();
        System.out.println(l);
    }
}

运行结果
一文了解java并发工具----------ThreadLocal

共享对象

如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得还是这个共享对象本身,还是有并发访问问题

不要强行使用

如果可以不用ThreadLocal就能解决问题,那么就不要强行使用。例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal。因为我们自己可能会忘记调用remove()方法等,造成内存泄露

上一篇:Java 并发编程:ThreadLocal 简单介绍


下一篇:ThreadLocal理解