Design of Thread Safety Class: Reasonable Use of ThreadLocal

Posted by pooker on Sun, 06 Oct 2019 20:08:48 +0200

1. ThreadLocal Class Definition
ThreadLocal, which means thread local variables, can provide a copy of instance variables for each thread. Each thread accesses and changes its own copy independently to ensure that there is no variable conflict between threads. ThreadLocal is a way to achieve thread safety by isolating shared variables from threads. There are three main methods:

T get(): Returns the value in the current thread copy in this thread local variable.
void remove (): Deletes the value of the current thread in the thread local variable.
void set (T value): Sets the value in the current thread copy in this thread local variable.

We can verify how ThreadLocal guarantees thread security through a simple experiment:

public class ThreadLocalTest implements Runnable {

    //Initialization Method of Rewriting ThreadLocal Class
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args){
        ThreadLocalTest threadLocalTest = new ThreadLocalTest();

        new Thread(threadLocalTest,"st-0").start();
        new Thread(threadLocalTest,"st-1").start();
        new Thread(threadLocalTest,"st-2").start();
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }
    }
}

Running the above code and intercepting part of the experimental results, we can see that each thread counts from the beginning, and the copies of variables changed by each thread will not affect other threads.

st-0: 1
st-1: 1
st-0: 2
st-1: 2
st-2: 1
st-0: 3
st-2: 2
st-1: 3

Two, the realization principle of ThreadLocal class
We can start from the source code of java.lang.ThreadLocal class to understand the implementation principle of ThreadLocal class.

1. ThreadLocal provides a ThreadLocalMap class, which is defined as follows. Different ThreadLocal variables have their own independent key-value pairs in ThreadLocalMap. The key value is an instance of ThreadLocal.

    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

2. ThreadLocal defines ThreadLocal Map, but the reference variables of ThreadLocal Map are stored in the Thread instance and coexist with each thread. A ThreadLocal Map can store multiple ThreadLocal variables.

public class Thread implements Runnable {

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

3. Looking at get and set methods, we can see that each thread has its own ThreadLocal Map, which saves its own ThreadLocal variables, and only reads and writes its own ThreadLocal variables.

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

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

3. ThreadLocal Performance in Thread Pool Mode
The resources occupied by each thread will be reclaimed by JVM after running, but in the thread pool mode, the thread will not die immediately after running, but will return to the thread pool to become idle thread, waiting for the next call. If ThreadLocal is used in thread pool mode, the ThreadLocalMap variable is not reclaimed after the thread ends, so the thread pool may have a conflict when it next processes the same ThreadLocal variable.

1. ThreadLocal performance without thread pool:

    public class ThreadLocalTest2 implements Runnable {

    //Initialization Method of Rewriting ThreadLocal Class
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args){
        ThreadLocalTest2 threadLocalTest = new ThreadLocalTest2();

        Thread t1 = new Thread(threadLocalTest,"st-1");
        Thread t2 = new Thread(threadLocalTest,"st-2");

        t1.start();

        while (t1.isAlive());

        if (!t1.isAlive()) {
            System.out.println("t1 is dead");
            t2.start();
        }
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }

        i.set(50);
    }
}

Operation results:

st-1: 96
st-1: 97
st-1: 98
st-1: 99
st-1: 100
t1 is dead
st-2: 1
st-2: 2
st-2: 3
st-2: 4

t2 thread starts at the end of t1 thread. It can be found that the i variable still starts at 0, and the operation of t1 thread will not affect t2 thread.

2. ThreadLocal performance when using thread pool:

public class ThreadPoolTest implements Runnable{

    //Initialization Method of Rewriting ThreadLocal Class
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(1);
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();

        Thread t1 = new Thread(threadPoolTest,"st-1");
        Thread t2 = new Thread(threadPoolTest,"st-2");

        Future<Integer> result = pool.submit(t1,1);
        if (result.get() ==  1){
            System.out.println("t1 runs off");
            pool.submit(t2);
        }
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }

        i.set(50);
    }
}

Operation results:

pool-1-thread-1: 96
pool-1-thread-1: 97
pool-1-thread-1: 98
pool-1-thread-1: 99
pool-1-thread-1: 100
t1 runs off
pool-1-thread-1: 51
pool-1-thread-1: 52
pool-1-thread-1: 53
pool-1-thread-1: 54

We find that after t1 thread runs, the i variable of t2 thread starts from 50, that is to say, the change of ThreadLocal variable of t1 thread affects the reading of t2 thread. ThreadLocal can not guarantee the isolation and security of data at this time, so in the thread pool mode, we need to carefully consider the way to achieve thread security with ThreadLocal. We can call remote method to release key after each thread ends.

Four, summary
ThreadLocal can effectively isolate conflicts between multiple threads accessing shared variables, but it is not suitable for scenarios where multiple threads communicate through shared data. In the online pool mode, ThreadLocal will not only cause data conflicts, but also it is possible that the ThreadLocal variable will survive in memory for a long time when the thread pool runs for a long time, resulting in a large amount of memory consumption.

Topics: Java jvm