并发-5-ThreadLocal

158 阅读3分钟

传统的数据库连接池管理代码如下:

public class ThreadLocalTest {
    //按照这种方法管理数据库连接,不会产生线程冲突问题,但是会消耗内存
    public void insert() throws SQLException {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.getConnection();
        //insert的业务逻辑
        connection.close();
    }
}


class ConnectionManager {
    //按照这种方法管理数据库连接的话,会产生线程冲突问题
    private Connection connection = null;

    public Connection getConnection() throws SQLException {
        //这里可能会产生同步问题,如果有两个线程A,B同时进入到这一行代码,发现connection都为null
        //线程A和线程B就会重复创建Connection
        if (connection == null) {
            connection = DriverManager.getConnection("xx");
        }
        return connection;
    }

    public void closeConnection() throws SQLException {
        //如果线程A在使用connection进行访问,线程2检测到connection!=null,进行关闭
        //这也是一种错误,应该对connection进行互斥访问
        if (connection != null) {
            connection.close();
        }
    }
}

  由于每次都需要在进行数据业务逻辑时才进行数据库连接创建,数据库连接其实是保存在方法栈中的,相互之间不影响,也就不存在了同步的问题。

ThreadLocal线程本地变量:

  ThreadLocal为变量在每个线程中都创建了一个副本,每个线程内部都有自己的一个变量,且在线程的任何地方都可以使用,线程之间不会相互影响   首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

使用案例:

  对于每个htpp请求,我们需要跟踪进入每个方法时调用的资源数量做计数器计数器,如果所有的请求共用一个计数器,那么这个计数器是全部请求的调用资源的次数 所以需要使用ThreadLocal来对每个线程存储对应的线程变量

public class ThreadLocalUse {

    //每个ThreadLocal为一个Map类型的数据结构,key为当前线程,value为所需要放入的值
    private static ThreadLocal<Long> longLocals = new ThreadLocal<>();
    private static ThreadLocal<String> stringLocals = new ThreadLocal<>();


    public void set() {
        longLocals.set(Thread.currentThread().getId());
        stringLocals.set(Thread.currentThread().getName());
    }

    public Long getLong() {
        return longLocals.get();
    }

    public String getString() {
        return stringLocals.get();
    }

    public static void main(String[] agrs) {
        ThreadLocalUse localUse = new ThreadLocalUse();
        localUse.set();

        new Thread(() -> {
            localUse.set();
            System.out.println(localUse.getLong());
            System.out.println(localUse.getString());
        }).start();

        System.out.println(localUse.getLong());
        System.out.println(localUse.getString());
    }
}

输出:

9
Thread-0
1
main

注意事项:

0.ThreadLocal是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal是各线程将值存入该线程的map中,以ThreadLocal自身作为key,需要用时获得的是该线程之前存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。

1.ThreadLocal不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set()到线程中的对象应该是线程自己使用的对象,其他线程是不需要访问的,也访问不到的。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

2.将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

3.很多封装的线程池(Executors)中的线程生命周期是很难预测的,这个时候如果使用ThreadLocal可能产生内存泄漏的问题