Java多线程-ThreadLocal(六)

java,多线程,threadlocal · 浏览次数 : 31

小编点评

**ThreadLocal 是如何实现线程安全和效率的?** ThreadLocal 是一种实现线程安全和效率的技术,它可以有效地解决线程创建和销毁之间的性能问题。 **它如何实现线程安全?** 1. **单一资源存储:** ThreadLocal 使用一个 `ThreadLocal` 类来存储资源。 `ThreadLocal` 是一个弱引用(WeakReference<ThreadLocal<?>>),这意味着即使资源被GC回收, `ThreadLocal` 仍然可以指向该资源。这有助于实现线程安全,因为即使资源被回收,线程仍然可以从 `ThreadLocal` 中获取该资源。 2. **线程安全:** `ThreadLocal` 使用 `synchronized` 块来确保多个线程访问相同的资源。这确保每个线程都使用到相同的资源,从而避免冲突。 **如何实现线程效率?** 1. **复用资源:** `ThreadLocal` 可以复用资源,这意味着多个线程可以从一个 `ThreadLocal` 中获取相同的资源。这可以显著提高性能,因为创建和销毁资源是一种耗时的操作。 2. **减少线程创建和销毁:** `ThreadLocal` 可以帮助减少线程创建和销毁之间的性能问题。当线程从 `ThreadLocal` 中获取资源时,如果资源已经存在,则不需要创建新的资源。 **ThreadLocal 的其他优点:** 1. **弱引用:** `ThreadLocal` 是一个弱引用,这意味着资源被 GC 回收后, `ThreadLocal` 仍然可以指向该资源。这可以确保线程在资源被回收之前继续使用该资源。 2. **线程安全:** `ThreadLocal` 使用 `synchronized` 块来确保线程安全。 3. **性能:** `ThreadLocal` 通常比传统的静态资源管理方案更快速。 **总结:** ThreadLocal 是一个非常有效的线程安全和效率技术。它可以有效地解决线程创建和销毁之间的性能问题,并通过减少线程创建和销毁之间的性能问题来提高应用程序的性能。

正文

为了提高CPU的利用率,工程师们创造了多线程。但是线程们说:要有光!(为了减少线程创建(T1启动)和销毁(T3切换)的时间),于是工程师们又接着创造了线程池ThreadPool。就这样就可以了吗?——不,工程师们并不满足于此,他们不把自己创造出来的线程给扒个底朝天决不罢手。

有了线程关键字解决线程安全问题,有了线程池解决效率问题,那还有什么问题是可以需要被解决的呢?——还真被这帮疯子攻城狮给找到了!

当多个线程共享同一个资源的时候,为了保证线程安全,有时不得不给资源加锁,例如使用Synchronized关键字实现同步锁。这本质上其实是一种时间换空间的搞法——用单一资源让不同的线程依次访问,从而实现内容安全可控。就像这样:

 

 但是,可以不可以反过来,将资源拷贝成多份副本的形式来同时访问,达到一种空间换时间的效果呢?当然可以,就像这样:

 

 

 

而这,就是ThreadLocal最核心的思想。

 

但这种方式在很多应用级开发的场景中用得真心不多,而且有些公司还禁止使用ThreadLocal,因为它搞不好还会带来一些负面影响。

其实,从拷贝若干副本这种功能来看,ThreadLocal是实现了在线程内部存储数据的能力的,而且相互之间还能通信。就像这样:

还是以代码的形式来解读一下ThreadLocal。有一个资源类Resource:

/**
 * 资源类
 */
public class Resource {
    private String name;
    private String value;

    public Resource(String name, String value) {
        super();
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

分别有ResuorceUtils1、ResuorceUtils2和ResuorceUtils3分别以不同的方式来连接资源,那么看看效率如何。

/**
 * 连接资源工具类,通过静态方式获得连接
 */
public class ResourceUtils1 {
    // 定义一个静态连接资源
    private static Resource resource = null;
    // 获取连接资源
    public static Resource getResource() {
        if(resource == null) {
            resource = new Resource("xiangwang", "123456");
        }
        return resource;
    }

    // 关闭连接资源
    public static void closeResource() {
        if(resource != null) {
            resource = null;
        }
    }
}



/**
 * 连接资源工具类,通过实例化方式获得连接
 */
public class ResourceUtils2 {
    // 定义一个连接资源
    private Resource resource = null;
    // 获取连接资源
    public Resource getResource() {
        if(resource == null) {
            resource = new Resource("xiangwang", "123456");
        }
        return resource;
    }

    // 关闭连接资源
    public void closeResource() {
        if(resource != null) {
            resource = null;
        }
    }
}



/**
 * 连接资源工具类,通过线程中的static Connection的副本方式获得连接
 */
public class ResourceUtils3 {
    // 定义一个静态连接资源
    private static Resource resource = null;
    private static ThreadLocal<Resource> resourceContainer = new ThreadLocal<Resource>();
    // 获取连接资源
    public static Resource getResource() {
        synchronized(ResourceManager.class) {
            resource = resourceContainer.get();
            if(resource == null) {
                resource = new Resource("xiangwang", "123456");
                resourceContainer.set(resource);
            }
            return resource;
        }
    }

    // 关闭连接资源
    public static void closeResource() {
        if(resource != null) {
            resource = null;
            resourceContainer.remove();
        }
    }
}



/**
 * 连接资源管理类
 */
public class ResourceManager {
    public void insert() {
        // 获取连接
        // System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + resource);
    }

    public void delete() {
        // 获取连接
        // System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + resource);
    }

    public void update() {
        // 获取连接
        // System.out.println("Dao.update()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.update()-->" + Thread.currentThread().getName() + resource);
    }

    public void select() {
        // 获取连接
        // System.out.println("Dao.select()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.select()-->" + Thread.currentThread().getName() + resource);
    }

    public void close() {
        ResourceUtils3.closeResource();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                ResourceManager rm = new ResourceManager();
                @Override
                public void run() {
                    rm.insert();
                    rm.delete();
                    rm.update();
                    rm.select();
                    rm.close();
                }
            }).start();
        }
    }
}

执行ResourceManager类中的main()方法后,可以清楚地看到:

第一种静态方式:大部分资源都能复用,但毫无规律;

第二种实例方式:即使是同一个线程,资源实例也不一样;

第三种ThreadLocal静态方式:相同的线程有相同的实例。

结论是:ThreadLocal实现了线程的资源复用。

 

也可以通过画图的方式来看清楚三者之间的不同:

这是静态方式下的资源管理:

 这是实例方式下的资源管理:

 这是ThreadLocal静态方式下的资源管理:

 

 

 理解了之后,再来看一个数据传递的例子,也就是ThreadLocal实现线程间通信的例子:

/**
 * 数据传递
 */
public class DataDeliver {
    static class Data1 {
        public void process() {
            Resource resource = new Resource("xiangwang", "123456");
            //将对象存储到ThreadLocal
            ResourceContextHolder.holder.set(resource);
            new Data2().process();
        }
    }

    static class Data2 {
        public void process() {
            Resource resource = ResourceContextHolder.holder.get();
            System.out.println("Data2拿到数据: " + resource.getName());
            new Data3().process();
        }
    }

    static class Data3 {
        public void process() {
            Resource resource = ResourceContextHolder.holder.get();
            System.out.println("Data3拿到数据: " + resource.getName());
        }
    }

    static class ResourceContextHolder {
        public static ThreadLocal<Resource> holder = new ThreadLocal<>();
    }

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

运行代码之后,可以看到Data1的数据都被Data2和Data3拿到了,就像这样:

 

 

 

ThreadLocal在实际应用级开发中较少使用,因为容易造成OOM:

1、由于ThreadLocal是一个弱引用(WeakReference<ThreadLocal<?>>),因此会很容易被GC回收;

2、但ThreadLocalMap的生命周期和Thread相同,这就会造成当key=null时,value却还存在,造成内存泄漏。所以,使用完ThreadLocal后需要显式调用remove操作(但很多码农不知道这一点)。

 Thread ThreadLocal ThreadLocalMap之间的关系:

1、Thread中有个属性用于存放ThreadLocalMap

2、ThreadLocalMap是ThreadLocal的静态内部类

3、ThreadLocalMap中保存的是Entry(键值对:键是ThreadLocal,值是自己设置的value)

 

与Java多线程-ThreadLocal(六)相似的内容:

Java多线程-ThreadLocal(六)

为了提高CPU的利用率,工程师们创造了多线程。但是线程们说:要有光!(为了减少线程创建(T1启动)和销毁(T3切换)的时间),于是工程师们又接着创造了线程池ThreadPool。就这样就可以了吗?——不,工程师们并不满足于此,他们不把自己创造出来的线程给扒个底朝天决不罢手。 有了线程关键字解决线程安

Java多线程-JUC-1(八)

前面把线程相关的生命周期、关键字、线程池(ThreadPool)、ThreadLocal、CAS、锁和AQS都讲完了,现在就剩下怎么来用多线程了。而要想用好多线程,其实是可以取一些巧的,比如JUC(好多面试官喜欢问的JUC,就是现在要讲的JUC)。JUC就是java.util.concurrent的

Java多线程

一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括: Java线程具有五中基本状态 新建状态(New):当线程对象对创建后,即进入了新建

Java多线程生成波场靓号

​ 玩区块链,手上没靓号怎么行。用网上的靓号生成器有一定的风险性,思来想去决定自己写一个。首先需要导入波场官方编辑 org.tron.trident utils

java多线程编程:你真的了解线程中断吗?

java.lang.Thread类有一个 interrupt 方法,该方法直接对线程调用。当被interrupt的线程正在sleep或wait时,会抛出 InterruptedException 异常。事实上, interrupt 方法只是改变目标线程的中断状态(interrupt status),...

杰哥教你面试之一百问系列:java多线程

java多线程是java面试中的高频问题,如何才能在面试中脱颖而出呢?熟读这里的一百个java多线程面试问题即可。 ### 1. **什么是线程?什么是进程?** **回答:** - 线程是操作系统能够进行调度的最小执行单位,它包含在进程中,共享进程的资源。 - 进程是一个正在执行中的程序,它包含了

JAVA多线程并发编程-避坑指南

本篇旨在基于编码规范、工作中积累的研发经验等,整理在多线程开发的过程中需要注意的部分,比如不考虑线程池参数、线程安全、死锁等问题,将会存在潜在极大的风险。并且对其进行根因分析,避免每天踩一坑,坑坑不一样。

Java多线程-线程生命周期(一)

如果要问我Java当中最难的部分是什么?最有意思的部分是什么?最多人讨论的部分是什么?那我会毫不犹豫地说:多线程。 Java多线程说它难,也不难,就是有点绕;说它简单,也不简单,需要理解的概念很多,尤其是很多底层知识,如数据结构、操作系统的部分。 Java多线程掌握得好,不仅仅只是对Java,对任何

Java多线程-线程关键字(二)

Java中和线程相关的关键字就两:volatile和synchronized。 volatile以前用得较少,以后会用得更少(后面解释)。它是一种非常轻量级的同步机制,它的三大特性是: 1、保证可见性,即强制将CPU高速缓存的数据立即写入主存,会导致其他CPU核中对应的高速缓存内容无效,就像这样:

Java多线程-ThreadPool线程池-1(三)

开完一趟车完整的过程是启动、行驶和停车,但老司机都知道,真正费油的不是行驶,而是长时间的怠速、频繁地踩刹车等动作。因为在速度切换的过程中,发送机要多做一些工作,当然就要多费一些油。 而一个Java线程完整的生命周期就包括: 1、T1:创建(启动) 2、T2:运行(行驶) 3、T3:销毁(停车) 而T