`

ThreadLocal的细节和设计模式

 
阅读更多

提要:

1.都知道ThreadLocal都是线程局部变量,可以比作是个Thread->T的MAP,那么有个问题了,如果一个类维护了一个TL的局部变量,随着不同的线程访问,这个TL会变得很大么?我们需要在线程结束前调用TL.remove来删除TL变量么,如果不删除会不会空间无法释放导致OOM呢?

2.在写某些会被多线程访问的代码时,某些实例变量需要做成线程私有,那么就会出现在使用这些变量时都使用threadLocal.get(),这样的junk code,有好的代码结构可以优化他么?

 

============================================================================

解答:

 

1.其实ThreadLocal并不把变量保存在自己里,而是保存到线程t里,

   摘自ThreadLocal

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

   

     摘自Thread

     

    ThreadLocal.ThreadLocalMap threadLocals = null;

    因此,TheadLocal本身并不保存变量,而是委托线程去保存。而这里又有个问题,这样做可以保证ThreadLocal不会受到线程的生命周期影响,我们也不需要显示remove。那么这里又有个问题了,线程Map里始终维护了TheadLocal->T的变量,如果维护ThreadLocal的对象被GC掉,线程本地变量里的ThreadLocal变量却依然被引用,并不会被gc。这样会不会有内存泄露呢?

   实际上是不会的,这个归功于WeakReference的使用

   WeakReference是一种引用容器,他虽然会维持R的引用,但是如果除了WeakReference外没有其他Object引用R,那么weakreference会在R被GC时,删除他。

    所以回到ThreadLocal,如果已经没有对象引用ThreadLocal,那么线程中的ThreadLocaMap就会踢掉这个被回收的ThreadLocal。

    详见Thread.ThreadLocalMap

   

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
.....
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
}

    可以看到Entry是一个TheadLocal的弱引用,其成员变量保存了映射的value,Entry[]是线程的所有ThreadLocal变量。类似一个LinkedHashMap。

 

2.如何避免使用ThreadLocal的线程安全类满屏的t.get()这样的junkcode

   在Ibatis中sqlMapClient配置为一个bean,但是每个线程在使用smc时总有自己的局部变量,例如Transaction,这样就跟我们的场景一样。但是我们并没有看到sqlMapClient使用t.get()这样的代码。他是怎么做到的呢:

   

 

    如图,sqlMapClient是全局共享的,他的queryForObject其实是委托一个sqlMapSessionImpl实现的,

sqlMapSessionImpl是线程成私有的,保存在smc的TheadLocal变量里的。

    摘自SqlMapClient

  protected SqlMapSessionImpl getLocalSqlMapSession() {
    SqlMapSessionImpl sqlMapSession = (SqlMapSessionImpl) localSqlMapSession.get();
    if (sqlMapSession == null || sqlMapSession.isClosed()) {
      sqlMapSession = new SqlMapSessionImpl(this);
      localSqlMapSession.set(sqlMapSession);
    }
    return sqlMapSession;
  }
 

 

      而sqlMapSessionImpl作为一个实例变量,他是不可能完成数据库操作的,他是委托了SqlMapExecutorDelegate的方法,SMED是在sqlMapClient里的变量,可以理解为是变相的回调操作。而SMSI里维护了一个SessionScope,这个是一个线程上下文里的变量,

摘自SqlMapSessionImpl

 

public class SqlMapSessionImpl implements SqlMapSession {

  protected SqlMapExecutorDelegate delegate;
  protected SessionScope sessionScope;
  protected boolean closed;
...
}
 

 

摘自SessionScope

 

public class SessionScope {
  private static long nextId;
  private long id;
  // Used by Any
  private SqlMapClient sqlMapClient;
  private SqlMapExecutor sqlMapExecutor;
  private SqlMapTransactionManager sqlMapTxMgr;
  private int requestStackDepth;
  // Used by TransactionManager
  private Transaction transaction;
  private TransactionState transactionState;
...
}
       SqlMapExecutorDelegate才是数据库执行的真正地方,那么既然要实现线程安全的操作,势必有个SqlMapExecutorDelegate不能再维护TheadLocal变量了,因此SqlMapExecutorDelegate的操作都带有SessionScope这个入参。

 

 

摘自SqlMapExecutorEelegate

 

  public Object queryForObject(SessionScope sessionScope, String id, Object paramObject) throws SQLException {
    return queryForObject(sessionScope, id, paramObject, null);
  }
 因此通过这样的回调,巧妙的解决了junkcode,让代码更加清晰。

 

 

================================================================================

模仿一下

 

我们需要抽象一个数据源DataProvider,DataProvider可以有很多实现类,例如FileDataProvider,MysqlDataProvider,这个DataProvider数据的获取方式变成Iterator的方式。

 

public abstract class DataProvider implements Iterator<Row> {
    public abstract Set<String> listFieldsName();
    public abstract Long size();
    public abstract void setPath(String path);
    public String getName() {
        return this.getClass().toString();
    }
    public abstract String getDesc();
    public void remove() {
        throw new UnsupportedOperationException();
    }
}
 

 

 那么junk-code的写法(代码只截取部分,有个意思)

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;


@Component

public class FileDataProvider extends DataProvider {

    private ThreadLocal<ReaderInfo> readerInfo = new ThreadLocal<ReaderInfo>();

    class ReaderInfo {
        public String csvFullPath = null;
        public BufferedReader reader = null;
        public File file = null;
        public boolean hasMore = false;
        public boolean isColumn = true;
        public List<String> columnNameList = null;
    }


    public boolean hasNext() {
        try {
            if (readerInfo.get().file == null)
                readerInfo.get().file = new File(readerInfo.get().csvFullPath);
            if (readerInfo.get().reader == null)
                readerInfo.get().reader = new BufferedReader(new FileReader(readerInfo.get().file));

            if (readerInfo.get().reader.ready()) {
                readerInfo.get().hasMore = true;

                if (readerInfo.get().isColumn) {
                    readerInfo.get().isColumn = false;
                    readerInfo.get().columnNameList = new ArrayList<String>();

                    String line = readerInfo.get().reader.readLine();
                    StringTokenizer st = new StringTokenizer(line, ",");
                    while (st.hasMoreTokens()) {
                        readerInfo.get().columnNameList.add(st.nextToken());
                    }

                }

                return true;
            } else {
                readerInfo.get().hasMore = false;
                readerInfo.get().reader.close();
                readerInfo.get().reader = null;
                return false;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
...
 
}
    

 

在hashNext中需要访问readerInfo里的path和file都用到了readerInfo.get()。

 

 

-------------以下是模仿sqlMapClient做的改造--------------------

 

1.把readerfino独立出来(变个名字sessionScope)

public class SessionScope {
    public String csvFullPath = null;
    public BufferedReader reader = null;
    public File file = null;
    public boolean hasMore = false;
    public boolean isColumn = true;
    public List<String> columnNameList = null;
}

 

2.编写一个使用sessionScope作为入参的读取器FileProviderExecutor

 

import com.alibaba.cainiao.hellomaven.impl.Field;
import com.alibaba.cainiao.hellomaven.impl.Row;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;


public class FileProviderExecutor {

    public boolean hasNext(SessionScope sessionScope) {
        try {
            if (sessionScope.file == null)
                sessionScope.file = new File(sessionScope.csvFullPath);
            if (sessionScope.reader == null)
                sessionScope.reader = new BufferedReader(new FileReader(sessionScope.file));

            if (sessionScope.reader.ready()) {
                sessionScope.hasMore = true;

                if (sessionScope.isColumn) {
                    sessionScope.isColumn = false;
                    sessionScope.columnNameList = new ArrayList<String>();

                    String line = sessionScope.reader.readLine();
                    StringTokenizer st = new StringTokenizer(line, ",");
                    while (st.hasMoreTokens()) {
                        sessionScope.columnNameList.add(st.nextToken());
                    }

                }

                return true;
            } else {
                sessionScope.hasMore = false;
                sessionScope.reader.close();
                sessionScope.reader = null;
                return false;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

...
}

 

3.写一个FileDataSessionProvider,维护sessionScope,和FileProviderExecutor,FPE使用构造函数传递进来(当然也可以吧FPE换成FileDataProvider)

public class FileDataSessionProvider {

    private SessionScope sessionScope;
    private FileProviderExecutor fileProviderExecutor = null;

    public FileDataSessionProvider(FileProviderExecutor executor) {
        this.fileProviderExecutor =  executor ;
        sessionScope = new SessionScope();

    }


    public Set<String> listFieldsName() {
        return fileProviderExecutor.listFieldsName(sessionScope);
    }


    public Long size() {
        return fileProviderExecutor.size(sessionScope);
    }


    public void setPath(String path) {
        this.sessionScope.csvFullPath = path;
    }


    public boolean hasNext() {
        return fileProviderExecutor.hasNext(sessionScope);
    }

    public Row next() {
        return fileProviderExecutor.next(sessionScope);
    }
}

 

     所有的函数委托Executor执行,传入参数sessionScope

 

4.最后,封装FileDataProvider,维护一个TheadLocal变量,里面存放FileDataSessionProvider,实现DataProvider,获取TheadLocal变量进行调用。

public class FileDataProvider extends DataProvider {

    FileProviderExecutor fileProviderExecutor = new FileProviderExecutor();

    private ThreadLocal<FileDataSessionProvider> fileDataSessionProviderThreadLocal = new ThreadLocal<FileDataSessionProvider>();

    private FileDataSessionProvider getLocalSessionProvider() {

        FileDataSessionProvider fileDataSessionProvider = fileDataSessionProviderThreadLocal.get();
        if (fileDataSessionProvider == null) {
            fileDataSessionProvider = new FileDataSessionProvider(this.fileProviderExecutor);
            fileDataSessionProviderThreadLocal.set(fileDataSessionProvider);
        }
        return fileDataSessionProvider;

    }

    @Override
    public Set<String> listFieldsName() {
        return this.getLocalSessionProvider().listFieldsName();
    }

    @Override
    public Long size() {
        return this.getLocalSessionProvider().size();
    }

    @Override
    public void setPath(String path) {
        this.getLocalSessionProvider().setPath(path);
    }

    @Override
    public String getDesc() {
        return "测试下";
    }

    public boolean hasNext() {
        return this.getLocalSessionProvider().hasNext();
    }

    public Row next() {
        return this.getLocalSessionProvider().next();
    }

    public FileProviderExecutor getFileProviderExecutor() {
        return fileProviderExecutor;
    }

    public void setFileProviderExecutor(FileProviderExecutor fileProviderExecutor) {
        this.fileProviderExecutor = fileProviderExecutor;
    }
}

 

getLocalSessionProvider()中,如果该线程没有访问过就创建sessionProvider,传入Executor。这样就完成了封装,绕过了对ThreadLocal变量内部成员变量的反复读取。这样的设计模式让代码层次更清晰,但是呢缺增加了代码量和理解难度,所以可以看情况选择使用。

 

不过这样的写法在设计模式里能找到对应的类型么?

 

FINISH

 

 

  • 大小: 24.2 KB
分享到:
评论

相关推荐

    java多线程设计模式

    Java多线程设计模式是Java开发中不可或缺的一部分,它涉及到如何在并发环境下高效、安全地组织程序执行...在提供的"java多线程设计模式.pdf"文件中,应该会涵盖更多细节和示例,有助于进一步理解这些模式的实现和应用。

    dao设计模式DAO 设计模式之事务界定疯.pdf

    在软件开发中,尤其是在基于数据库的应用程序中,数据访问层(Data Access Object, DAO)设计模式是一种常用的模式,用于简化和标准化对数据库的操作。该模式的主要目的是将业务逻辑与数据访问逻辑分离,使得应用...

    设计模式原理与应用之创建型模式.pptx

    创建型模式是设计模式中的一个重要类别,它关注于对象的创建过程,帮助开发者封装创建细节,使系统独立于具体对象的创建方式。 1. **Singleton(单例模式)** 单例模式确保一个类只有一个实例,并提供全局访问点...

    druid 源码分析 逐层详解

    最后,Druid的源码中还体现了多种语言特性,比如Interface和Abstract的使用,FunctionP的实现,ClassLoader的运用,以及ThreadExecutes和ThreadLocal的应用。这些语言特性的应用有助于构建出高效和线程安全的代码。 ...

    day020-继承加强和设计模式代码和笔记.rar

    5. 设计模式:(框架中使用,是程序设计的高级思想) 1. 单例模式:(重点) 1. 概念:设计一个类,这个类只能创建一个对象。(限制创建对象对象数量) 2. 怎么设计? 控制创建对象的数量 =&gt; 创建...

    Java 55 道面试题及答案.docx

    适配器模式、装饰器模式和代理模式都是常见的设计模式。适配器模式用于桥接两个接口,而装饰器模式用于在不修改类的情况下给类增加新的功能。代理模式则是增加一个额外的中间层,以便支持分配、控制或智能访问。 11...

    java细节

    了解常见的设计模式(如单例、工厂、观察者等)并能在实际项目中灵活应用,能够提高代码的可重用性和可扩展性。 10. 性能优化: Java程序员应关注JVM的性能调优,包括堆内存大小设置、垃圾回收参数调整、方法区和...

    阿里巴巴面试经验分享,经历七轮面试,最终定级P6

    阿里巴巴面试经验分享,经历七轮面试,最终定级 P6 ...本篇文章涵盖了基础知识、框架和设计模式、项目经验和解决方案、分布式系统和消息队列、系统设计和优化等多方面的知识点,对阿里巴巴的面试经历进行了总结和解释。

    Spring5源码分析心得

    Spring框架作为一款优秀的Java企业级应用开发框架,其内部集成了多种设计模式,不仅有助于提高系统的可维护性和扩展性,还能够让开发者更好地理解和掌握面向对象设计原则。下面我们将详细介绍Spring中常用的设计模式...

    S1_Java内测题目

    设计模式是解决常见问题的模板,是经验的总结,对于写出可维护和扩展的代码非常重要。 总之,"S1_Java内测题目"涵盖了从基础到高级的Java编程知识,包括但不限于语法、面向对象、集合框架、异常处理、JVM、并发编程...

    java面试基础

    设计模式是软件工程的基石,面试中常出现的有单例、工厂、观察者、装饰者、适配器等23种设计模式,理解其应用场景和优缺点非常重要。 最后,面试者还应关注一些实际项目经验,如如何处理高并发场景,数据库优化,...

    基于Java的多线程数据通信程序设计.zip

    6. **设计模式**:在实现多线程数据通信程序时,设计模式如生产者-消费者模型、读者-写者模型、工作窃取等能有效解决并发问题。例如,使用`BlockingQueue`实现生产者-消费者模型,可以高效地协调数据生产与消费的...

    学习java的积点忽略之处

    9. **设计模式**:设计模式是解决常见编程问题的模板,如单例、工厂、观察者等。学习并应用设计模式能够提升代码的可读性和可维护性,初学者往往忽视了这一点。 10. **JVM内部机制**:了解JVM的工作原理,包括类...

    阿里巴巴Java开发手册.zip

    设计模式是提高代码复用性和扩展性的关键。手册列举了常见的设计模式,并提供了在实际开发中的应用示例,如单例模式、工厂模式、观察者模式等,帮助开发者提升设计能力。 七、单元测试 单元测试被视为保证代码质量...

    Java 解惑(细致实用)

    - 避免滥用设计模式,每个模式都有其适用场景。 9. **JVM优化** - 了解JVM的工作原理,包括类加载、内存模型、垃圾回收策略等,有助于进行性能调优。 - 调整JVM参数,如堆大小、新生代和老年代比例,以及GC策略...

    SaaS模式分析

    例如,通过使用Spring拦截器进行租户识别,并通过ThreadLocal传递给后端,使得数据库和缓存层对应用层透明。 2. **数据隔离**:为了保护各租户之间的数据安全,SaaS系统需要在数据库访问层对SQL语句进行改写,以...

    Java Interview Questions/Java面试题

    Java面试题是每个Java开发者在求职过程中都会遇到的挑战,涵盖范围广泛,包括语言基础、集合框架、多线程、JVM、设计模式等多个方面。本文将基于标题"Java Interview Questions/Java面试题"和提供的标签"源码"、...

    线程相关的单例模式

    在软件开发中,单例模式是一种常用的面向对象设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在处理资源管理、控制并发以及提高性能等方面有着广泛的应用。当涉及到线程相关的问题时...

    张孝祥正在整理Java就业面试题大全

    6. **设计模式**:理解常见的23种设计模式,如工厂模式、单例模式、建造者模式、适配器模式等,并能灵活应用于实际项目中。 7. **JVM内存模型**:理解堆、栈、方法区、本地方法栈的概念,垃圾回收机制,以及性能...

    Java面试指南2016

    求职者需要理解每种设计模式的使用场景和优缺点,并能够在项目中灵活运用。 书中还提到了Java并发编程中的重要概念,如ThreadLocal的基本原理、Java内存泄露的原因、GC如何判断对象失去引用等。Java的并发机制是一...

Global site tag (gtag.js) - Google Analytics