当前位置:   article > 正文

java 并发 - ThreadLocal详解:它是怎么做到的线程隔离_threadlocal 作为变量的线程隔离方式,其内部是如何做的

threadlocal 作为变量的线程隔离方式,其内部是如何做的

ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量导致数据不一致的现象。

1. ThreadLocal使用场景和理解

1.1. 数据库连接管理

看一个简单的数据库处理类,逻辑很简单一个是打开连接,一个是关闭连接。

class ConnectionManager {
    private static Connection connect = null;

    public static Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public static void closeConnection() {
        if (connect != null)
            connect.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们回顾一下多线程下操作共享变量安全性的保证:

互斥同步:synchroniezd 和 reentrantLock
非阻塞同步:CAS、AtomicXXX
无同步方案:栈封闭、本地存储(Thread Local)。

对于上面的代码,因为有共享变量,多线程下可能会有安全问题。
安全问题:一个线程openConnection的同时,另一个变量在closeConnection;
同步问题:不互斥,有多个线程同时进入openConnection方法,多次创建了connect。

思考一个问题:需不需要互斥同步。
假设每个线程都有自己的connect变量,各个线程线程之间拿到connect之后,相互之间是没有依赖的。即一个线程修改了这个connect,不会影响其他线程对这个connect的使用。
 
因为每个线程使用的是对应CPU下的缓存,而不是内存中的数据。

但这会导致服务器压力过大,因为会在方法中频繁的创建数据连接。
 

ThreadLocal登场

ThreadLocal会在每个线程中对该变量创建一个副本,即每个线程都会有这样一个副本,互不影响。这样就不会造成线程安全和性能问题。

常用的例子

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

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

注意下ThreadLocal的修饰符

如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

同时,threadLocal副本会造成内存的占用。

 

1.2. ThreadLocal造成内存泄露的问题

看下列代码:使用线程池来操作ThreadLocal。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(5000 * 4);
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

如果使用线程池操作ThreadLocal会造成内存泄漏

内存泄漏:指一个对象不使用了,但是一直占据着内存,具体的:对象不使用了,但是还被其他对象引用着,就会导致GC时不能回收此对象(根的可达性分析),这种情况就是内存泄漏。

因为对于线程池中没有销毁的线程,里面总是存在着<ThreadLocal, LocalVariable> 的强引用,而对于ThreadLocalMap 对于key虽然是弱引用,但是强引用不释放,弱引用就会一直有值,同时创建的Local Variable对象也不会释放,就造成了内存泄漏。

具体地,泄漏的内存 = coreThreadNum * LocalVariable对象的大小

ThreadLocal提供了一个清除线程中对象的方法, 即 remove。来避免内存泄漏的情况。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

 

1.3. 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;  
}  

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

 

1.4. java 开发手册中推荐的 ThreadLocal

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

调用

DateUtils.df.get().format(new Date());
  • 1

 

1.5. 每个线程维护了一个“序列号”

当我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来时,使用ThreadLocal。

为每一个线程维护一个序列号。

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

 
 

2. ThreadLocal原理

2.1. ThreadLocal如何实现的线程隔离

看一下set和get

先获取当前线程的map,然后set值,进行更新。如果这个线程没有map就先创建这个线程的map,并set值。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

获取当前线程的Map,如果不为空,则获取这个线程对应的值,否则初始化。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

    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;
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

***分析了get和set方法,对于ThreadLocal的线程隔离的实现其实很简单,就是为每个线程创建一个map,其中key存储着线程本身,value就是操作的共享变量。 ***

每个线程在get时,如果此线程没有set,则获取的是null。

 
 
 
参考:
https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/294554
推荐阅读
相关标签
  

闽ICP备14008679号