Java并发28:ThreadLocal学习笔记-简介、基本方法及应用场景

本章主要对ThreadLocal进行学习。

1.初始ThreadLocal

ThreadLocal又称为线程本地变量线程局部变量,来源于JDK1.2版本。

简单来说,每个线程都单独存放一个ThreadLocal变量副本线程之间互不干扰

ThreadLocal主要区别于线程之间的共享变量

  • 线程共享变量:多个线程共同访问这个变量,存在数据争用
  • 线程本地变量:每个线程单独存放这个变量的副本不存在数据争用。

下面,通过一段简单的代码来演示线程本地变量线程共享变量区别

自定义一个同时包含两种变量的自定义类型:

/**
 * <p>共享变量、线程本地变量--示例</p>
 *
 * @author hanchao 2018/3/21 23:46
 **/
static class MyNum {
    //共享变量,多个线程共享
    int num;
    //本地变量,每个线程单独创建一个副本
    ThreadLocal<Integer> threadLocalNum = new ThreadLocal<Integer>();

    public MyNum(int num, Integer threadLocalNum) {
        this.num = num;
        this.threadLocalNum.set(threadLocalNum);
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public ThreadLocal<Integer> getThreadLocalNum() {
        return threadLocalNum;
    }
}

测试代码:

 //构造
MyNum myNum = new MyNum(0, new Integer(0));
System.out.println("线程[" + Thread.currentThread().getName()
        + "]----num:" + myNum.getNum() + ",threadLocalNum:" + myNum.getThreadLocalNum().get().intValue() + "\n");
//多线程运行
for (int i = 0; i < 4; i++) {
    new Thread(() -> {
        //每个线程中执行加1计算
        myNum.setNum(myNum.getNum() + 1);
        //打印结果
        System.out.println("线程[" + Thread.currentThread().getName()
                + "]----num: " + myNum.getNum());

        //ThreadLocal只能在自己的线程中设置值
        if (myNum.getThreadLocalNum().get() != null) {
            myNum.getThreadLocalNum().set(myNum.getThreadLocalNum().get().intValue() + 1);
            //打印结果
            System.out.println("线程[" + Thread.currentThread().getName()
                    + "]----threadLocalNum: " + myNum.getThreadLocalNum().get().intValue());
        } else {
            System.out.println("线程[" + Thread.currentThread().getName()
                    + "]----threadLocalNum is null ,threadLocalNum to " + 1);
            myNum.getThreadLocalNum().set(1);
        }
    }).start();
    Thread.sleep(100);
    System.out.println();
}
Thread.sleep(100);
System.out.println("线程[" + Thread.currentThread().getName()
        + "]----num:" + myNum.getNum() + ",threadLocalNum:" + myNum.getThreadLocalNum().get().intValue());

System.out.println("\n线程共享变量在多个线程中共享;线程本地变量每个线程独有一份副本,互补影响;main也是一个线程。");

 运行结果:

线程[main]----num:0,threadLocalNum:0

线程[Thread-0]----num: 1
线程[Thread-0]----threadLocalNum is null ,threadLocalNum to 1

线程[Thread-1]----num: 2
线程[Thread-1]----threadLocalNum is null ,threadLocalNum to 1

线程[Thread-2]----num: 3
线程[Thread-2]----threadLocalNum is null ,threadLocalNum to 1

线程[Thread-3]----num: 4
线程[Thread-3]----threadLocalNum is null ,threadLocalNum to 1

线程[main]----num:4,threadLocalNum:0

线程共享变量在多个线程中共享;线程本地变量每个线程独有一份副本,互补影响;main也是一个线程。

从运行结果可知:

  • 线程共享变量经过四个线程的+1操作最终为4。
  • 线程本地变量在每个线程中的值都是独立的。
  • 线程本地变量在每个线程中都需要进行初始值设置。

2.ThreadLocal的基本方法

ThreadLocal的基本方法如下:

  • ThreadLocal():构造方法,初始值为null。
  • get():获取当前线程中此线程本地变量的值。
  • set(value):为当前线程中此线程本地变量赋值。
  • remove():移除当前线程中此线程本地变量的值。
  • initialValue():返回当前线程中此线程本地变量的初值。常用来Override(重写)以设置初始值。

下面通过实例代码对这些方法进行练习:

ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
//ThreadLocal默认值为null
System.out.println("ThreadLocal默认值为null,所以需要先set()才能使用--" + threadLocal.get() + "\n");

//ThreadLocal在每个线程中都需要单独赋值
Thread.sleep(100);
threadLocal.set(1);
System.out.println("通过get()获取当前线程中的值,线程[" + Thread.currentThread().getName() + "] value =" + threadLocal.get());
new Thread(() -> {
    System.out.println("每个线程中需要单独赋值,线程[" + Thread.currentThread().getName() + "] value =" + threadLocal.get());
    threadLocal.set(1);
    System.out.println("每个线程中需要单独赋值,线程[" + Thread.currentThread().getName() + "] value =" + threadLocal.get() + "\n");
}).start();

//通过remove()删除当前线程的值
Thread.sleep(100);
threadLocal.remove();
System.out.println("通过remove()删除当前线程的值,线程[" + Thread.currentThread().getName() + "] value =" + threadLocal.get());

Thread.sleep(100);
//重写protected initialValue()方法用来设置初始值
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "Hello World!";
    }
};
System.out.println("\n重写protected initialValue()方法用来设置初始值," + Thread.currentThread().getName() + "---" + stringThreadLocal.get());

运行结果:

ThreadLocal默认值为null,所以需要先set()才能使用--null

通过get()获取当前线程中的值,线程[main] value =1
每个线程中需要单独赋值,线程[Thread-4] value =null
每个线程中需要单独赋值,线程[Thread-4] value =1

通过remove()删除当前线程的值,线程[main] value =null

重写protected initialValue()方法用来设置初始值,main---Hello World!

3.ThreadLocal的应用场景

主要学习以下两种应用场景:

  • 数据库连接管理
  • 会话管理

3.1.数据库连接管理

场景:

定义一个数据库连接工具类来管理数据库连接。

分析:

  • 如果使用普通的共享变量来定义这个连接(Connection),并不能满足并发的需求,因为这些连接是多个线程共享的。
  • 如果为每个Dao层都新建一个连接(Connection),则每个连接都需要消耗cpu资源和内存资源。
  • 使用ThreadLocal能够避免多线程的争用问题;而且为每个线程建立副本,能够节省很多资源。

代码:

自定义数据库连接管理类:

/**
* <p>Title: 一个自定义数据库连接工具</p>
*
* @author 韩超 2018/3/22 14:18
*/
static class MyDBUtils {
//        String driver = "oracle.jdbc.driver.OracleDriver";//oracle
//        String url = "jdbc:oracle:thin:@localhost1521:test";//oracle
static String driver = "com.mysql.jdbc.Driver";
static String url = "jdbc:mysql://localhost:3306/exam?useSSL=false";
static String username = "root";
static String password = "root";

//每个连接线程一个连接实例
static ThreadLocal<Connection> connection = new ThreadLocal<Connection>() {
    //重写ThreadLocal的initialValue方法,获取连接
    @Override
    protected Connection initialValue() {
        Connection connection = null;
        try {
            //加载JDBC驱动
            Class.forName(driver);
            //获得连接
            connection = DriverManager.getConnection(url, username, password);
            System.out.println(Thread.currentThread().getName() + " 获取了一个MySql连接...是否关闭---" + connection.isClosed());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }
};

测试代码:

//ThreadLocal常用场景01:数据库连接
System.out.println("\n=========ThreadLocal常用场景01:数据库连接");
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        //获取
        Connection connection = MyDBUtils.getConnection();
        //关闭连接
        try {
            connection.close();
            System.out.println(Thread.currentThread().getName() + " 关闭了连接.是否关闭---" + connection.isClosed());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }).start();
}

运行结果:

=========ThreadLocal常用场景01:数据库连接
Thread-7 获取了一个MySql连接...是否关闭---false
Thread-9 获取了一个MySql连接...是否关闭---false
Thread-8 获取了一个MySql连接...是否关闭---false
Thread-6 获取了一个MySql连接...是否关闭---false
Thread-5 获取了一个MySql连接...是否关闭---false
Thread-5 关闭了连接.是否关闭---true
Thread-6 关闭了连接.是否关闭---true
Thread-7 关闭了连接.是否关闭---true
Thread-8 关闭了连接.是否关闭---true
Thread-9 关闭了连接.是否关闭---true

3.2.session管理

关于session管理本人并没有进行实际编码练习,现将其他博友的代码贴到这里,作为参考。

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException { 
  Session s = (Session) threadSession.get(); 
  try { 
    if (s == null) { 
      s = getSessionFactory().openSession(); 
      threadSession.set(s); 
    } 
  } catch (HibernateException ex) { 
    throw new InfrastructureException(ex); 
  } 
  return s; 
} 

 

posted @ 2021-08-26 17:14  姚春辉  阅读(48)  评论(0编辑  收藏  举报