Java多线程——线程封闭

线程封闭:当访问共享的可变数据时,一般须要同步。一种避免同步的方式就是不共享数据。若是仅在单线程内访问数据,就不须要同步,这种技术称为线程封闭(thread  confinement)java

  线程封闭技术一个常见的应用就是JDBC的Connection对象,JDBC规范并无要求Connection对象必须是线程安全的,在服务器应用程序中,线程从链接池获取一个Connection对象,使用完以后将对象返还给链接池。下面介绍几种线程封闭技术:mysql

  一、Ad-hoc线程封闭sql

  Ad-hoc线程封闭是指,维护线程的封闭性的职责彻底由程序实现承担,是很是脆弱的,所以在程序中尽可能少使用,通常使用更强的线程封闭技术,好比栈封闭或者ThreadLocal类。安全

二、栈封闭  服务器

  栈封闭是线程封闭的一种特列,在栈封闭中,只能经过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行栈中,其余线程没法访问这个栈,栈封闭也称为线程内部使用或者线程局部使用。简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。因此局部变量是不被多个线程所共享的,也就不会出现并发问题。因此能用局部变量就别用全局的变量,全局变量容易引发并发问题。多线程

  好比下面的例子:并发

public class Snippet {
    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
    
        // animals被封闭在方法中,不要使它们逸出!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }
}

  在loadTheArk中实例化一个TreeSet对象,并将该对象的一个引用保存到animals中。此时,只有一个引用指向集合animals,这个引用被封闭到局部变量中,所以也被封闭到局部变量中。然而,若是发布了对集合animals(或者该对象中的任何内部数据)的引用,那么封闭性将被破坏,并致使对象animals的逸出。ide

三、ThreadLocal类this

  维持线程封闭性的一种更加规范方法是使用ThreadLocal类,这个类能使线程中某个值与保存值的对象关联起来。ThreadLocal类提供了get和set等访问接口或者方法,这些方法为每一个使用该变量的线程都存在一份独立的副本,所以get老是放回当前执行线程在调用set设置的最新值。看一下下面代码例子:spa

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            Connection conn = null;
            try {
                conn = DriverManager.getConnection(
                        "jdbc:mysql://localhost:3306/test", "username",
                        "password");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection conn) {
        connectionHolder.set(conn);
    }
}

  经过调用ConnectionManager.getConnection()方法,每一个线程获取到的,都是本身独立拥有的一个的Connection对象副本,第一次获取时,是经过initialValue()方法的返回值来设置值的。经过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的彻底隔离。在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码很是类似。

  每一个线程是怎么和Connection对象副本绑定的?这个对象副本保存在哪里。当某个线程初次调用ThreadLocal类的get方法时,就会调用initialValue来获取初始值,从概念上看,咱们能够将ThreadLocal<T>视为包含了Map<thread, T>对象,其中保存了特定于该线程的值,可是ThreadLocal的实现并不是如此,这样只是为了咱们方便理解而已。

  下面咱们来分析一下ThreadLocal的源码。ThreadLocal类的方法很简单,只有四个,分别为set,get,remove, initialValue,从字面上咱们也能理解这些方法的做用。

  public T get():返回当前线程所对应的局部变量。

  public void set(T arg0):设置当前线程局部变量的值。  

  public void remove():将当前线程局部变量的值删除,目的是为了减小内存的占用,该方法是JDK 5.0新增的方法。注意,当线程结束后,对应该线程的局部变量将自动被垃圾回收,因此显式调用该方法清除线程的局部变量并非必须的操做,但它能够加快内存回收的速度。

  protected T initialValue(): 对当线程局部变量进行初始化,并返回该初始值。是protected 属性,显然是让子类进行对其覆盖重写的,只有第一次调用set和get方法时才调用。  

  下面咱们对这四个方法的源码进行分析,看看ThreadLocal类是如何实现这种“为每一个线程提供不一样的变量拷贝”。

3.1 set方法

  如下是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();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

  从set方法中能够看到,首先获取当前线程:Thread arg1 = Thread.currentThread();

  再获取当前线程的ThreadLocalMap:ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);

  判断ThreadLocalMap是否为空,不为空,则以键值对的形式设置值,key为this,value就是局部变量的副本,this是当前线程持有的ThreadLocal类实例化对象。

  假如为空,则经过createMap方法建立。

  咱们看下getMap和createMap方法的源码:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  从代码上已经写的很是清楚,每一个线程都有本身的局部变量的副本,该副本是存在ThreadLocalMap 中,其中键值就是ThreadLocal类实例化对象。也就是说每一个线程都拥有本身的ThreadLocalMap,ThreadLocalMap保存的就是局部变量副本。咱们看一下java.lang.Thread源码。

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

3.2 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();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

  从代码上看,前两步和set方法是一个样的,分别获取当前线程和当前线程的ThreadLocalMap,第三步判断ThreadLocalMap是否为空,不为空根据this键值获取value,为空调用setInitialValue()方法。

  如下是setInitialValue方法代码:

private T setInitialValue() {
        Object arg0 = this.initialValue();
        Thread arg1 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
        if (arg2 != null) {
            arg2.set(this, arg0);
        } else {
            this.createMap(arg1, arg0);
        }
        return arg0;
}

  在setInitialValue里调用了initialValue()方法,也就是子类要重写覆盖的方法,对应上面的例子的代码是:

protected Connection initialValue() {  
            Connection conn = null;  
            try {  
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
            return conn;  
}

  而后获取当前线程和当前线程的ThreadLocalMap,ThreadLocalMap为空则调用createMap,不然调用set方法。

3.3 总结

  ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这固然和前面set()方法的代码是相呼应的。

  进一步地,咱们能够建立不一样的ThreadLocal实例来实现多个变量在不一样线程间的访问隔离,为何能够这么作?由于不一样的ThreadLocal对象做为不一样键,固然也能够在线程的ThreadLocalMap对象中设置不一样的值了。经过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对同样,仅此而已。

  也就说,每一个线程都有一个ThreadLocalMap,该线程访问到某个局部变量,且该局部变量是用ThreadLocal类进行声明时,该线程就会new ThreadLocal(),而后将该ThreadLocal类的对象做为key值,所对应的局部变量做为value值保存到ThreadLocalMap中。当线程访问多个ThreadLocal类进行声明局部变量时,在ThreadLocalMap中就有多个键值对。而每一个线程都有本身的ThreadLocalMap,从而达到隔离的目的了。

  当某个线程终止后,该线程里的ThreadLocalMap也被回收了,因此彻底不用担忧内存泄漏的问题。

  假如多线程访问的对象实例是单例的,或者说只能建立一个,那就老老实实的使用同步机制(synchronized)了.

相关文章
相关标签/搜索