• Java多线程(4):ThreadLocal


    您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~

     

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

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

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

     

     

     

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

     

     

     

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

     

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

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

     

     

     

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

    复制代码
    /**
     * 资源类
     *
     * @author 湘王
     */
    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、ResuorceUtils2ResuorceUtils3分别以不同的方式来连接资源,那么看看效率如何。

    复制代码
    /**
     * 连接资源工具类,通过静态方式获得连接
     *
     * @author 湘王
     */
    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;
            }
        }
    }
    
    
    
    /**
     * 连接资源工具类,通过实例化方式获得连接
     *
     * @author 湘王
     */
    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的副本方式获得连接
     *
     * @author 湘王
     */
    public class ResourceUtils3 {
        // 定义一个静态连接资源
        private static Resource resource = null;
        private static ThreadLocal resourceContainer = new ThreadLocal();
        // 获取连接资源
        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();
            }
        }
    }
    
    
    
    /**
     * 连接资源管理类
     *
     * @author 湘王
     */
    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实现线程间通信的例子:

    复制代码
    /**
     * 数据传递
     *
     * @author 湘王
     */
    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 holder = new ThreadLocal<>();
        }
    
        public static void main(String[] args) {
            new Data1().process();
        }
    }
    复制代码

     

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

     

     

     

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

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

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

     

     


     

     

    感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~

     

  • 相关阅读:
    Ubuntu 22.04LTS + 深度学习环境安装全流程
    [附源码]Python计算机毕业设计Django仓储综合管理系统
    HCNP Routing&Switching之RSTP
    redis雪崩、穿透、击穿
    递归的本质
    【linux】shell:简单的shell脚本练习
    TS的声明文件
    朋友圈营销攻略:定时发送、互动与延迟评论
    老卫带你学---leetcode刷题(347. 前 K 个高频元素)
    Machine Learning Pre-Basics
  • 原文地址:https://www.cnblogs.com/xiangwang1111/p/16830014.html