Основы интервью: анализ принципа ThreadLocal [длинная статья в бутике]

Java
Основы интервью: анализ принципа ThreadLocal [длинная статья в бутике]

На основе OpenJDK 12

Свинец

Эта статья в основном хочет знать два места:

  1. Экземпляры ThreadLocal кажутся общими для нескольких потоков, но на самом деле они независимы друг от друга Как это реализовано?
  2. Действительно ли неправильное использование ThreadLocal вызывает OOM? Если да, то в чем причина?

Первый взглядОфициальный API ThreadLocalИнтерпретируется как:

Этот класс предоставляет локальные переменные потока. Эти переменные отличаются от своих обычных аналогов, потому что каждый поток, обращающийся к переменной (через свой метод get или set), имеет свою собственную локальную переменную, которая не зависит от инициализированной копии переменной. поток, который обращается к одному (через свой метод get или set), имеет свою собственную, независимо инициализированную копию переменной.]. Экземпляры ThreadLocal обычно представляют собой закрытые статические поля в классах, которые хотят связать состояние с потоком (например, идентификатор пользователя или идентификатор транзакции).

Вероятно, это означает две вещи:

  • ThreadLocal обеспечивает особый способ доступа к переменной: переменная, к которой осуществляется доступ, принадлежит текущему потоку, то есть гарантирует, что переменные каждого потока различны, а переменные, полученные одним и тем же потоком где-либо, непротиворечивы, что называется изоляцией потоков. .
  • Если вы хотите использовать ThreadLocal, он обычно определяется как частный статический тип, и, на мой взгляд, лучше определить его как закрытый статический окончательный тип.

Посмотрите на кусок кода:

// 代码来自:
// http://tutorials.jenkov.com/java-concurrency/threadlocal.html
public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

        @Override
        public void run() {
            //注意这里 set的值是run函数的内部变量,如果是MyRunnable的全局变量
            //则无法起到线程隔离的作用
            threadLocal.set((int) (Math.random() * 100D));
            try {
                //sleep两秒的作用是让thread2 set操作在thread1的输出之前执行
                //如果线程之间是共用threadLocal,则thread2 set操作会覆盖掉thread1的set操作
                //从而两者的输出都是thread2 set的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
            System.out.println(threadLocal.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }
}

Выходной результат:

thread1 start
thread2 start
38
thread1 join
78
thread2 join

Функция засыпания в течение двух секунд в MyRunnable run состоит в том, чтобы разрешить выполнение операции установки потока 2 до вывода потока 1. Если threadLocal используется совместно между потоками, операция установки потока 2 перезапишет операцию установки потока 1, а вывод обоих установлено значение thread2. , поэтому на выходе должно быть то же значение.

Однако по результатам выполнения кода threadLocals потоков thread1 и thread2 различаются, то есть достигается изоляция потоков.

Как экземпляры ThreadLocal независимы между потоками?

Взгляните на метод set ThreadLocal:

public void set(T value) {
    //currentThread是个native方法,会返回对当前执行线程对象的引用。
    Thread t = Thread.currentThread();
    //getMap 返回线程自身的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //把value set到线程自身的ThreadLocalMap中了
        map.set(this, value);
    } else {
        //线程自身的ThreadLocalMap未初始化,则先初始化,再set
        createMap(t, value);
    }
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//Thread类中
//ThreadLocalMapset的set方法未执行深拷贝,需要注意传递值的类型
ThreadLocal.ThreadLocalMap threadLocals = null;

Как видно из кода, при установке значение будет добавлено к соответствующему потоку в соответствии со ссылкой на объект Thread. Но значением set является все тот же объект, а поскольку передается один и тот же объект, возникает другая проблема: проблема передачи значения параметра и передачи ссылки.

базовый тип

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        // MyRunnable 全局变量
        int random;

        @Override
        public void run() {
            random = (int) (Math.random() * 100D);
            threadLocal.set(random);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(threadLocal.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}

Выходной результат:

thread1 start
thread2 start
//两个值不同
16
thread1 join
75
thread2 join

Из вывода видно, что они изолированы.

тип ссылки

глобальная ссылка

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        // MyRunnable 全局变量
        Obj obj = new Obj();

        @Override
        public void run() {
            obj.value = (int) (Math.random() * 100D);
            threadLocal.set(obj);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(((Obj) threadLocal.get()).value);
        }

        class Obj {
            int value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}

Выходной результат:

thread1 start
thread2 start
//两个值相同
36
36
thread1 join
thread2 join

Из результатов вывода, когда значение операции set является глобальной переменной MyRunnable и является ссылочным типом, оно не может играть роль изоляции.

местная ссылка

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        //Obj obj = new Obj();
        @Override
        public void run() {
            Obj obj = new Obj();
            obj.value = (int) (Math.random() * 100D);
            threadLocal.set(obj);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(((Obj) threadLocal.get()).value);
        }

        class Obj {
            int value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}

Выходной результат:

thread1 start
thread2 start
//两个值不同
12
19
thread1 join
thread2 join

По результатам вывода локальные ссылки могут быть изолированы друг от друга.

Здесь видно, что ThreadLocal только привязывает установленное значение или ссылку к текущему потоку, но не выполняет соответствующее глубокое копирование, поэтому изоляция потока, которую хочет сделать ThreadLocal, должна быть локальной переменной базового типа или выполняться.

ThreadLocal OOM?

Взгляните на Entry внутри ThreadLocalMap:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Как видно из кода, Entry наследует WeakReference и устанавливает для ThreadLocal значение WeakReference, а для value — сильную ссылку. То есть: когда нет строгой ссылки на переменную ThreadLocal, ее можно переработать.

Тем не менее, все еще есть проблема: ThreadLocalMap поддерживает сопоставление между переменными ThreadLocal и конкретными экземплярами.При повторном использовании переменной ThreadLocal ключ карты становится нулевым, а Entry все еще находится в ThreadLocalMap, поэтому эти не поддающиеся очистке Entry вызовут память утечка.

Собственные методы удаления и установки ThreadLocal не могут справиться с ситуацией, когда сам ThreadLocal имеет значение null, потому что код напрямую принимает свойство threadLocalHashCode ThreadLocal, поэтому, если сам ThreadLocal уже имеет значение null, то вызов remove и set сообщит об исключении нулевого указателя (java.lang .Исключение нулевого указателя).

Поэтому при использовании ThreadLocal не забудьте удалить после использования (метод удаления установит значение Entry и самого Entry равным нулю и очистит его).

Адрес кода JDK 12 ThreadLocal:GitHub.com/jiankun король…

Личный публичный аккаунт WeChat:

Персональный гитхаб:

github.com/jiankunking

личный блог:

jiankunking.com