Java并发(六):线程本地变量ThreadLocal、再聊线程池

本文来自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7451464,转载请注明。

ThreadLocal

首先说明ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。

public class ConnectionManager {  

    /** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */  
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();  

    public static Connection getCurrConnection() {  
        // 获取当前线程内共享的Connection  
        Connection conn = threadLocal.get();  
        try {  
            // 判断连接是否可用  
            if(conn == null || conn.isClosed()) {  
                // 创建新的Connection赋值给conn(略)  
                // 保存Connection  
                threadLocal.set(conn);  
            }  
        } catch (SQLException e) {  
            // 异常处理  
        }  
        return conn;  
    }  

    /** 
     * 关闭当前数据库连接 
     */  
    public static void close() {  
        // 获取当前线程内共享的Connection  
        Connection conn = threadLocal.get();  
        try {  
            // 判断是否已经关闭  
            if(conn != null && !conn.isClosed()) {  
                // 关闭资源  
                conn.close();  
                // 移除Connection  
                threadLocal.remove();  
                conn = null;  
            }  
        } catch (SQLException e) {  
            // 异常处理  
        }  
    }  
} 

这样处理的好处:

1、统一管理Connection;
2、不需要显示传参Connection,代码更优雅;
3、降低耦合性。


ThreadLocal有四个方法,分别为:

  • protected T initialValue()

返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

返回:
返回此线程局部变量的初始值


  • public T get()

返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
返回:
此线程局部变量的当前线程的值


  • public void set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
参数:
value - 存储在此线程局部变量的当前线程副本中的值。


  • public void remove()

移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
很多人对ThreadLocal存在一定的误解,说ThreadLocal中有一个全局的Map,set时执行map.put(Thread.currentThread(), value),get和remove时也同理,但SUN的大师们是否是如此实现的,我们只能去看源码了。

set方法:

/** 
 * Sets the current thread's copy of this thread-local variable 
 * to the specified value.  Most subclasses will have no need to 
 * override this method, relying solely on the {@link #initialValue} 
 * method to set the values of thread-locals. 
 * 
 * @param value the value to be stored in the current thread's copy of 
 *        this thread-local. 
 */  
public void set(T value) {  
    // 获取当前线程对象  
    Thread t = Thread.currentThread();  
    // 获取当前线程本地变量Map  
    ThreadLocalMap map = getMap(t);  
    // map不为空  
    if (map != null)  
        // 存值  
        map.set(this, value);  
    else  
        // 创建一个当前线程本地变量Map  
        createMap(t, value);  
}  

/** 
 * Get the map associated with a ThreadLocal. Overridden in 
 * InheritableThreadLocal. 
 * 
 * @param  t the current thread 
 * @return the map 
 */  
ThreadLocalMap getMap(Thread t) {  
    // 获取当前线程的本地变量Map  
    return t.threadLocals;  
}  

这里注意,ThreadLocal中是有一个Map,但这个Map不是我们平时使用的Map,而是ThreadLocalMap,ThreadLocalMap是ThreadLocal的一个内部类,不对外使用的。当使用ThreadLocal存值时,首先是获取到当前线程对象,然后获取到当前线程本地变量Map,最后将当前使用的ThreadLocal和传入的值放到Map中,也就是说ThreadLocalMap中存的值是[ThreadLocal对象, 存放的值],这样做的好处是,每个线程都对应一个本地变量的Map,所以一个线程可以存在多个线程本地变量。

get方法:

/** 
 * Returns the value in the current thread's copy of this 
 * thread-local variable.  If the variable has no value for the 
 * current thread, it is first initialized to the value returned 
 * by an invocation of the {@link #initialValue} method. 
 * 
 * @return the current thread's value of this thread-local 
 */  
public T get() {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null)  
            return (T)e.value;  
    }  
    // 如果值为空,则返回初始值  
    return setInitialValue();  
}  

有了之前set方法的分析,get方法也同理,需要说明的是,如果没有进行过set操作,那从ThreadLocalMap中拿到的值就是null,这时get方法会返回初始值,也就是调用initialValue()方法,ThreadLocal中这个方法默认返回null。当我们有需要第一次get时就能得到一个值时,可以继承ThreadLocal,并且覆盖initialValue()方法。


再聊线程池

池化

这里池化并不是深度学习中的池化,而是将资源交给池来管理的这一过程。我们在开发中经常回接触到池化资源的技术,最常见的当然是数据库连接池,以及我们今天要讲的线程池,那这种池化资源的特点和好处是什么呢?

特点

  • 通常管理昂贵的资源,如连接、线程等

  • 资源的创建和销毁交给池,调用者不需要关心

好处

  • 资源重复利用,提高响应速度

  • 资源可管理,可监控

线程池分析

类结构

这里写图片描述

这里面的实现类涉及到三个:

  • ForkJoinPool:一个类似于Map/Reduce模型的框架,线程级的,详细可有去看我之前写的文章Fork/Join-Java并行计算框架

  • ThreadPoolExecutor:这是Java线程池的实现,也是本文的主角,Executors提供的几种线程池主要使用该类。

  • ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,添加了调度功能。

ThreadPoolExecutor参数

  • int corePoolSize //线程池基本大小

  • int maximumPoolSize //线程池最大大小

  • long keepAliveTime //保持活动时间

  • TimeUnit unit //保持活动时间单位

  • BlockingQueue workQueue //工作队列

  • ThreadFactory threadFactory //线程工厂

  • RejectedExecutionHandler handler //驳回回调

这些参数这样描述起来很空洞,下面结合执行任务的流程来看一下。

ThreadPoolExecutor执行任务流程

当我们调用execute方法时,这个流程就开始了,请看下图:

这里写图片描述

当线程池大小 >= corePoolSize 且 队列未满时,这时线程池使用者与线程池之间构成了一个生产者-消费者模型。线程池使用者生产任务,线程池消费任务,任务存储在BlockingQueue中,注意这里入队使用的是offer,当队列满的时候,直接返回false,而不会等待,有关BlockingQueue可以看我之前写的文章阻塞队列BlockingQueue

这里写图片描述.

keepAliveTime

当线程处于空闲状态时,线程池需要对它们进行回收,避免浪费资源。但空闲多长时间回收呢,keepAliveTime就是用来设置这个时间的。默认情况下,最终会保留corePoolSize个线程避免回收,即使它们是空闲的,以备不时之需。但我们也可以改变这种行为,通过设置allowCoreThreadTimeOut(true)。

RejectedExecutionHandler

当队列满 且 线程池大小 >= maximumPoolSize时会触发驳回,因为这时线程池已经不能响应新提交的任务,驳回时就会回调这个接口rejectedExecution方法,JDK默认提供了4种驳回策略,代码比较简单,直接上代码分析,具体使用何种策略,应该根据业务场景来选择,线程池的默认策略是AbortPolicy。

ThreadPoolExecutor.AbortPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 直接抛出运行时异常
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}
ThreadPoolExecutor.CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        // 转成同步调用
        r.run();
    }
}
ThreadPoolExecutor.DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 空实现,意味着直接丢弃了
}
ThreadPoolExecutor.DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        // 取出队首,丢弃
        e.getQueue().poll();
        // 重新提交
        e.execute(r);
    }
}

Hook methods

ThreadPoolExecutor预留了以下三个方法,我们可以通过继承该类来做一些扩展,比如监控、日志等等。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Thread t, Runnable r) { }
protected void terminated() { }

ThreadPoolExecutor状态

线程池的工作流程我们应该大致清楚了,其内部同时维护了一个状态,现在来看一下每种状态对于任务会造成什么影响以及状态之间的流转。

  • RUNNING

    • 初始状态,接受新任务并且处理已经在队列中的任务。
  • SHUTDOWN

    • 不接受新任务,但处理队列中的任务。
  • STOP

    • 不接受新任务,不处理排队的任务,并中断正在进行的任务。
  • TIDYING

    • 所有任务已终止,workerCount为零,线程转换到状态TIDYING,这时回调terminate()方法。
  • TERMINATED

    • 终态,terminated()执行完成。

这里写图片描述

上图是这5种状态间的流转,可以看到它们是单向的、不可逆的。

总结

现在我们在回过头来去看Executors中提供的几种线程池(fixed、cached、single),如果你能回答出下面几个问题,说明你明白了线程池。

1、为什么newFixedThreadPool中要将corePoolSize和maximumPoolSize设置成一样?
2、为什么newFixedThreadPool中队列使用LinkedBlockingQueue?
3、为什么newFixedThreadPool中keepAliveTime会设置成0?
4、为什么newCachedThreadPool中要将corePoolSize设置成0?
5、为什么newCachedThreadPool中队列使用SynchronousQueue?
6、为什么newSingleThreadExecutor中使用DelegatedExecutorService去包装ThreadPoolExecutor?

可能到这里会有人问,讲了这么多,我应该如何去选择线程池?线程池应该设置多大?没有固定的答案,只有适合的答案,下面说一下我的理解:

  • 关于线程池大小问题,可以参考这个公式,仅仅是参考而已。
启动线程数 = [ 任务执行时间 / ( 任务执行时间 - IO等待时间 ) ] x CPU内核数
  • 在控制线程池大小的基础上,尽量使用有界队列并且设置大小,避免OOM。

  • 设置合理的驳回策略,适用于你的业务。

本文来自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7451464,转载请注明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值