首页 > 其他分享 >ThreadLocal解析

ThreadLocal解析

时间:2024-03-02 22:56:01浏览次数:33  
标签:00 01 ThreadLocal 线程 date new 解析 public

ThreadLocal解析

目录

1. 两大使用场景——ThreadLocal的用途

image-20240302182431066

典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

image-20240302182615382

进化1:使用两个线程打印日期

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 描述:两个线程打印日期
 */
public class ThreadLocalNormalUsage00 {
    public static void main(String[] args) {
        new Thread( ()->{
            String date = new ThreadLocalNormalUsage00().date(10);
            System.out.println(date);
        }).start();

        new Thread(() ->{
            String date = new ThreadLocalNormalUsage00().date(104707);
            System.out.println(date);
        }).start();
    }

    public String date(int second){
        Date date = new Date(1000*second);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}

打印结果:

1970-01-01 08:00:10
1970-01-02 13:05:07

进化2:使用10个线程打印日期

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 *  10个线程打印日期
 */
public class ThreadLocalNormalUsage01 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            Thread.sleep(100);
        }
    }

    /**
     * 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
     * @param second 毫秒
     * @return
     */
    public String date(int second){
        Date date = new Date(1000*second);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}

打印结果:

1970-01-01 08:00:00
1970-01-01 08:00:01
1970-01-01 08:00:02
1970-01-01 08:00:03
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:07
1970-01-01 08:00:08
1970-01-01 08:00:09
...

进化3:1000个线程打印日期,将进化2中的代码for循环终止值改为1000,可执行,不出错,但是执行效率非常慢。

进化4:使用线程池优化1000个线程打印日期,因为每次线程执行都需要创建SimpleDateFormat对象,将该对象提取到外层。

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

/**
 *  1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });



        }
        // 线程执行完毕,停止线程
        threadPool.shutdown();
    }

    /**
     * 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
     * @param second 毫秒
     * @return
     */
    public String date(int second){
        Date date = new Date(1000*second);
        return dateFormat.format(date);
    }
}

打印结果:

1970-01-01 08:00:05
1970-01-01 08:00:05
1970-01-01 08:00:05
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:03
1970-01-01 08:00:07

此时,已经出现问题,按顺序打印出来的值有重复数值,逻辑上i变量传入date,应不会有重复的值。

image-20240302193028877

原因就在于多线程下,SimpleDateFormat并非是线程安全的,因为SimpleDateFormat在格式化和解析日期时间时,使用了全局的日历(Calendar)对象,这个对象可以被多个线程访问到。当多个线程同时访问SimpleDateFormat时,可能会对日历对象进行修改,导致格式化和解析的结果不一致,这就是线程不安全的问题。

image-20240302193420705

进化5:使用Synchronize锁解决线程不安全场景

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

/**
 *  加锁来解决线程安全问题
 */
public class ThreadLocalNormalUsage04 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage04().date(finalI);
                    System.out.println(date);
                }
            });



        }
        // 线程执行完毕,停止线程
        threadPool.shutdown();
    }

    /**
     * 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
     * @param second 毫秒
     * @return
     */
    public String date(int second){
        Date date = new Date(1000*second);
        String s = null;
        //使用类锁
        synchronized (ThreadLocalNormalUsage04.class){
            s = dateFormat.format(date);
        }
        return s;
    }
}

打印结果:

1970-01-01 08:00:00
1970-01-01 08:00:02
1970-01-01 08:00:06
1970-01-01 08:00:03
1970-01-01 08:00:12
1970-01-01 08:00:11
1970-01-01 08:00:10
1970-01-01 08:00:01
......

从结果中可以看到,已经可以实现该功能。但使用synchronized会使得每一个线程排队依次执行format方法。效率大打折扣。

进化6:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存

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

/**
 *  利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });



        }
        // 线程执行完毕,停止线程
        threadPool.shutdown();
    }

    /**
     * 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
     * @param second 毫秒
     * @return
     */
    public String date(int second){
        Date date = new Date(1000*second);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    // 结合Lambda表达式改写
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(()->
        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );
}

总结:SimpleDateFormat的进化之路

  1. 2个线程分别用自己的SimpleDateFormat,这没有问题。
  2. 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,虽然写法不优雅(应该复用对象),但勉强可以接受。
  3. 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)
  4. 所有的线程都共用同一个SimpleDateForamat对象
  5. 这是线程不安全的,出现了并发安全问题。
  6. 我们可以选择加锁,加锁后结果正常,但是效率低
  7. 在这里更好的解决方案是ThreadLocal
  8. Lambda表达式改写优化
  1. 线程安全
  2. 没有Synchronize带来的性能问题,完全可以并行执行。因为每个线程内都有自己独享的对象,所以不同的线程之间不会相互共享,也就不存在线程安全问题

典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

image-20240302195803809

目标:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

image-20240302195859018

image-20240302195959000

  • 在此基础上可以演进,使用UserMap,但是Map是线程不安全的。

    image-20240302200052341

  • 可以使用Synchronized,也可以使用ConcurrentHashMap,但是无论怎么用,势必会对性能有所影响。

image-20240302200149364

  • 使用ThreadLocal

image-20240302200232503

image-20240302200331008

image-20240302200353921

案例:使用ThreadLocal存储用户对象信息

public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {

    public void process() {
        User user = new User("admin");
        //放入对象
        UserContextHolder.holder.set(user);
        new Service2().process();
    }

}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2 拿到用户名:" + user.name);

    }


}


class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();

}


class User {

    String name;

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

打印信息:

Service2 拿到用户名:admin

流程图如下

image-20240302201623092

场景总结

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  2. 在任何方法中都可以轻松获取到该对象

image-20240302201910329

如何选择场景是使用initialValue还是set

image-20240302202434803

image-20240302202454610

2.使用ThreadLocal带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效地利用内存、节省开销:相比于每个任务都新建一个SimpleDateFormat、显然使用ThreadLocal可以节省内存和开销。
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要每次都传相同的参数。ThreadLocal使得代码耦合度更低,更优雅。

3.主要方法介绍

image-20240302214014369

initialValue():初始化。

  1. 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
  2. 当线程第一次使用get方法访问变量时,在调用此方法,除非线程先调用了Set方法,在这种情况下,不会为线程调用本initialValue方法。
  3. 通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法。
  4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

get():

先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

注意:这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中

image-20240302214917192

set():与setInitialValue类似

image-20240302215325906

4.原理、源码

image-20240302212955390

每一个Thread对象都持有一个ThreadLocalMap成员变量。

image-20240302215648874

  • 冲突:HashMap(拉链法)

image-20240302215734173

  • ThreadLocalMap:使用线性探测法

image-20240302215942670

5.ThreadLocal注意点

  • 内存泄露

    • 什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收。

    • 弱引用的特点,如果这个对象被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收

    • 所以弱引用不会阻止GC,因此这个弱引用的好处体现

image-20240302220846779

此处的super(k)查看其父类为WeakReference(),弱引用。

image-20240302220917837

image-20240302221109773

image-20240302221303192

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

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

6. 空指针问题

使用包装类,而非基础类。如果第8行get()方法的返回对象是long,由于包装类拆箱,而包装类为null(setInitialValue初始化返回为null),导致NPE

public class ThreadLocalNPE {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();

    public void set(){
        longLocal.set(Thread.currentThread().getId());
    }
	
    // 注意使用包装类,而非基础类
    public Long get(){
        return longLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        // 主线程线程id是1
//        threadLocalNPE.set();
//        System.out.println(threadLocalNPE.get());

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread.start();
    }
}

image-20240302222248951

image-20240302222504206

实际应用场景——在Spring中实践

  • DateTimeContextHolder类,看到里面用到了ThreadLocal。

image-20240302223308158

  • RequestContextHolder中,每次HTTP请求都对应一个线程,线程之间相互隔离。如getRequestAttributes()这就是ThreadLocal的典型应用场景。

image-20240302222812662

image-20240302223139354

标签:00,01,ThreadLocal,线程,date,new,解析,public
From: https://www.cnblogs.com/shine-rainbow/p/18049413

相关文章

  • .NET 全能 Cron 表达式解析库(支持 Cron 所有特性)
    前言今天大姚给大家分享一个.NET全能Cron表达式解析类库,支持Cron所有特性:TimeCrontab。Cron表达式介绍Cron表达式是一种用于配置定时任务的时间表达式。它由一系列字段组成,每个字段代表任务在不同时间维度的调度规则。Cron表达式常用于各种系统中,如操作系统的定时任务、......
  • scrapy数据解析
    importscrapyclassDouSpider(scrapy.Spider):name="dou"#allowed_domains=["www.douban.com"]start_urls=["https://www.douban.com/doulist/113652271/"]defparse(self,response):div_=response......
  • NetCore 动态解析Razor代码
    第一步: Nuget引入:RazorEngine.NetCore 第二步:添加引用usingRazorEngine;usingRazorEngine.Templating; 第三步:代码实现模版替换publicclassFormModel:PageModel{publicstringHtmlCompile{set;get;}=string.Empty;publicvoidOnGet([From......
  • 掌握字符与字符串:C语言中的神奇函数解析(三)
    ✨✨欢迎大家来到贝蒂大讲堂✨✨......
  • LWIP RAW接口TCP与UDP部分函数解析
    RAWTCP接口tcp_input()函数voidtcp_input(structpbuf*p,structnetif*inp) --->staticerr_ttcp_process(structtcp_pcb*pcb) --->staticvoidtcp_receive(structtcp_pcb*pcb) --->>TCP_EVENT_RECV(pcb,recv_data,ERR_OK,err);//调用用户注册......
  • 解决Nginx代理转发中HTTP到HTTPS跳转问题的技术方案解析
    在进行Nginx代理转发时,经常会遇到HTTP到HTTPS跳转的问题,特别是在某些情况下,即使在程序中明确指定了使用HTTPS协议,仍然会出现跳转到HTTP的情况。本文将介绍一种解决这个问题的技术方案,并对问题的原因进行分析。问题描述在进行Nginx代理转发时,配置了HTTPS支持,但在程序中发起请求时......
  • Stable Diffusion 解析:探寻 AI 绘画背后的科技神秘
    AI绘画发展史在谈论StableDiffusion之前,有必要先了解AI绘画的发展历程。早在2012年,华人科学家吴恩达领导的团队训练出了当时世界上最大的深度学习网络。这个网络能够自主学习识别猫等物体,并在短短三天时间内绘制出了一张模糊但可辨识的猫图。尽管这张图片很模糊,但它展示......
  • 解析HTTP错误码400 Bad Request及其常见原因与解决方法
    解析HTTP错误码400BadRequest及其常见原因与解决方法1.引言在进行web开发过程中,我们经常会遇到各种HTTP错误码。HTTP错误码用于表示服务器对请求的响应状态,帮助我们定位和解决问题。本文将重点解析HTTP错误码400BadRequest,探讨其常见原因和解决方法。HTTP错误码的作用和分类......
  • 解析HTTP错误码400 Bad Request及其常见原因与解决方法
    解析HTTP错误码400BadRequest及其常见原因与解决方法1.引言在进行web开发过程中,我们经常会遇到各种HTTP错误码。HTTP错误码用于表示服务器对请求的响应状态,帮助我们定位和解决问题。本文将重点解析HTTP错误码400BadRequest,探讨其常见原因和解决方法。HTTP错误码的作用和分类......
  • ConcurrentHashMap 核心源码解析
    废话不多说,直接看代码类名与HashMap很相似,数组、链表结构几乎相同,都实现了Map接口,继承了AbstractMap抽象类,大多数的方法也都是相同的publicclassConcurrentHashMap<K,V>extendsAbstractMap<K,V>implementsConcurrentMap<K,V>,Serializable核心方法Node方法......