penglongli / blog

18 stars 1 forks source link

Java 线程的封闭性与 ThreadLocal #111

Open penglongli opened 6 years ago

penglongli commented 6 years ago

线程的封闭性

当多线程共享可变的数据时,通常需要使用同步来保证并发安全。如果仅在单线程内访问数据,就不需要同步,这就是线程的封闭性。

栈封闭

其实我们在写 Web 程序的时候,用到很多的就是栈封闭。举例如下:

static class Fruit {
    private String name;

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

static Fruit getFruit(String name) {
    // fruit 被封闭在方法中
    Fruit fruit = new Fruit();
    fruit.setName(name);

    return fruit;
}

如上所示,fruit 实例被封闭进方法中,多线程环境下并不会影响 fruit

ThreadLocal

ThreadLocal 对象通常用于防止对“可变的单实例变量(Singleton)”或全局变量进行共享。使用场景通常为:当每个线程都需要分配一个临时对象,并且希望对象没有同步问题,则可以用 ThreadLocal 类

如:在数据库查询的时候,多个查询对应于多个线程,每个线程执行查询需要 Connection 对象。由于 JDBC 的 Connection 不是线程安全的,因此将其保存到 ThreadLocal 中让每个线程都有各自的 Connection 对象。

public class ConnectionHelper {

    private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            try {
                Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
                String DB_URL = "jdbc:mysql://localhost:3306/database";
                return DriverManager.getConnection(DB_URL, "root", "root");
            } catch (SQLException e) {
                throw new RuntimeException("Unable to acquire Connection, e");
            } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

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

}

上述我们创建了 ThreadLocal<Connection> 的一个泛型实例,然后用下述验证:

public class Test {

    static ConnectionHelper connectionHelper = new ConnectionHelper();

    static class MyThread extends Thread {

        @Override
        public void run() {
            System.out.println(connectionHelper.getConnection());
        }
    }

    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();

        t1.start();
        t2.start();
    }
}

可以发现,t1 和 t2 打印出来的 Connection 实例不为 NULL 也不相同。

即为每一个线程创建了一个 Connection 对象

ThreadLocal 原理分析

当某个线程初次调用 ThreadLocal.get() 方法时,会首先调用 initValue 来获取初始值。

我们点击上述的 connectionHolder.get() 方法

// 首先通过 Thread.currentThread() 获取当前线程
// 通过传递当前 Thread,获取当前 Thread 的 threadLocals 变量
// 如果变量非空,则传递 ThreadLocal 当前对象的引用来获取 value
// 如果为空,则会调用 initValue() 方法来设置值
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

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

从上述可以看出来,我们的数据是存储在了线程的一个变量里,所以其是线程安全的。

这里需要说的是

线程池中使用 ThreadLocal

由于我们存储的 ThreadLocal 生命周期是随着线程结束而结束的,在线程池中会出现这种情况:

线程永远不会结束,则 ThreadLocal 对应线程的数据越来越多,并且一直被引用不会被 GC,会造成内存泄露的情况。

// TODO

关于这块回头再补充下示例