【Java】ThreadLocalの詳細解析

ThreadLocalの総合分析

予備知識

  • 特定の Javase および JavaWeb 基盤があること
  • 同期キーワードについてよく理解する
  • HashMap に精通している
  • JDBC テクノロジーに精通している

学習目標

  • ThreadLocal の導入を理解する
  • ThreadLocal のアプリケーション シナリオをマスターする
  • ThreadLocal の内部構造を理解する
  • ThreadLocal のコアメソッドのソースコードを理解する
  • ThreadLocalMapのソースコードを理解する

1. ThreadLocal の概要

1.1 正式導入

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
    
    
    ...

公式 Java ドキュメントの説明より: ThreadLocal クラスは、スレッド内にローカル変数を提供するために使用されます。この変数がマルチスレッド環境でアクセスされる (get および set メソッドを通じてアクセスされる) 場合、各スレッドの変数が他のスレッドの変数から相対的に独立していることが保証されます。ThreadLocal インスタンスは通常、プライベート静的タイプであり、スレッドとスレッド コンテキストを関連付けるために使用されます。

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
1. 线程并发: 在多线程并发的场景下
2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离: 每个线程的变量都是独立的,不会互相影响

1.2 基本的な使い方

1.2.1 一般的な方法

使用する前に、ThreadLocal の一般的なメソッドをいくつか理解しましょう

メソッド宣言 説明する
ThreadLocal() ThreadLocal オブジェクトを作成する
public void set(T 値) 現在のスレッドにバインドされたローカル変数を設定します
public T get() 現在のスレッドによってバインドされているローカル変数を取得します
パブリック void 削除() 現在のスレッドによってバインドされているローカル変数を削除します

1.2.2 使用例

我们来看下面这个案例	, 感受一下ThreadLocal 线程隔离的特点: 
public class MyDemo {
    
    
    private String content;

    private String getContent() {
    
    
        return content;
    }

    private void setContent(String content) {
    
    
        this.content = content;
    }

    public static void main(String[] args) {
    
    
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
    
    
            Thread thread = new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

印刷結果:

1574149020726

結果から、複数のスレッドが同じ変数にアクセスした場合に例外が発生し、スレッド間のデータが分離されていないことがわかります。この問題を解決するために ThreadLocal を使用する例を見てみましょう。

public class MyDemo {
    
    

    private static ThreadLocal<String> tl = new ThreadLocal<>();

    private String content;

    private String getContent() {
    
    
        return tl.get();
    }

    private void setContent(String content) {
    
    
         tl.set(content);
    }

    public static void main(String[] args) {
    
    
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
    
    
            Thread thread = new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

印刷結果:

画像-20230813162503280

結果から判断すると、これは複数のスレッド間のデータ分離の問題に対する良い解決策であり、非常に便利です。

1.3 ThreadLocal クラスと synchronized キーワード

1.3.1 同期同期方法

ここにいる友人の中には、上記の例ではロックを追加することでこの機能を実現できると考える人もいるかもしれません。まず、同期されたコード ブロックによって達成される効果を見てみましょう。

public class Demo02 {
    
    
    
    private String content;

    public String getContent() {
    
    
        return content;
    }

    public void setContent(String content) {
    
    
        this.content = content;
    }

    public static void main(String[] args) {
    
    
        Demo02 demo02 = new Demo02();
        
        for (int i = 0; i < 5; i++) {
    
    
            Thread t = new Thread(){
    
    
                @Override
                public void run() {
    
    
                    synchronized (Demo02.class){
    
    
                        demo02.setContent(Thread.currentThread().getName() + "的数据");
                        System.out.println("-------------------------------------");
                        String content = demo02.getContent();
                        System.out.println(Thread.currentThread().getName() + "--->" + content);
                    }
                }
            };
            t.setName("线程" + i);
            t.start();
        }
    }
}

印刷結果:

1578321788844

結果から、ロックによって確かにこの問題を解決できることがわかりますが、ここではマルチスレッドでのデータ共有の問題ではなく、スレッドのデータ分離の問題を強調します。この場合、synchronized キーワードを使用するのは不適切です。

1.3.2 ThreadLocal と同期の違い

ThreadLocal モードと synchronized キーワードはどちらも変数へのマルチスレッド同時アクセスの問題に対処するために使用されますが、問題に対する 2 つの視点と考え方は異なります。

同期した スレッドローカル
原理 同期メカニズムは「時間を空間と交換する」方法を採用しており、異なるスレッドがアクセスのためにキューに入ることができるように変数のコピーのみを提供します。 ThreadLocal は「空間を時間に交換する」方式を採用し、各スレッドに変数のコピーを提供することで、相互に干渉することなく同時アクセスを実現します。
集中 複数のスレッド間のリソースへのアクセスの同期 マルチスレッドでは、各スレッド間のデータは互いに分離されます。
总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。

2. アプリケーションシナリオ_ビジネスケース

上記の紹介を通じて、ThreadLocal の特徴を基本的に理解しました。しかし、それは正確には何に使われているのでしょうか?次に、トランザクション操作のケースを見てみましょう。

2.1 転送の場合

2.1.1 シーンの構築

ここでは、最初に単純な送金シナリオを構築します。データ テーブル アカウントがあり、2 人のユーザー Jack と Rose が含まれており、ユーザー Jack がユーザー Rose に送金します。

このケースの実装では主に mysql データベース、JDBC、C3P0 フレームワークを使用します。詳細なコードは次のとおりです。

(1) プロジェクトの構成

1574045241145

(2) データの準備

-- 使用数据库
use test;
-- 创建一张账户表
create table account(
	id int primary key auto_increment,
	name varchar(20),
	money double
);
-- 初始化数据
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 0);

(3) C3P0 設定ファイルとツール

<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
 <!--  连接参数 -->
 <property name="driverClass">com.mysql.jdbc.Driver</property>
 <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
 <property name="user">root</property>
 <property name="password">1234</property>
 
 <!-- 连接池参数 -->
 <property name="initialPoolSize">5</property>
 <property name="maxPoolSize">10</property>
 <property name="checkoutTimeout">3000</property>
</default-config>

</c3p0-config>

(4) ツールクラス: JdbcUtils

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    
    
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
    
    
        return ds.getConnection();
    }
    //释放资源
    public static void release(AutoCloseable... ios){
    
    
        for (AutoCloseable io : ios) {
    
    
            if(io != null){
    
    
                try {
    
    
                    io.close();
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    public static void commitAndClose(Connection conn) {
    
    
        try {
    
    
            if(conn != null){
    
    
                //提交事务
                conn.commit();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose(Connection conn) {
    
    
        try {
    
    
            if(conn != null){
    
    
                //回滚事务
                conn.rollback();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }
}

(5) dao レイヤーコード: AccountDao

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {
    
    

    public void out(String outUser, int money) throws SQLException {
    
    
        String sql = "update account set money = money - ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm,conn);
    }

    public void in(String inUser, int money) throws SQLException {
    
    
        String sql = "update account set money = money + ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm,conn);
    }
}

(6) サービス層コード: AccountService

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;

public class AccountService {
    
    

    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();
        try {
    
    
            // 转出
            ad.out(outUser, money);
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

(7) Web層コード: AccountWeb

package com.itheima.transfer.web;

import com.itheima.transfer.service.AccountService;

public class AccountWeb {
    
    

    public static void main(String[] args) {
    
    
        // 模拟数据 : Jack 给 Rose 转账 100
        String outUser = "Jack";
        String inUser = "Rose";
        int money = 100;

        AccountService as = new AccountService();
        boolean result = as.transfer(outUser, inUser, money);

        if (result == false) {
    
    
            System.out.println("转账失败!");
        } else {
    
    
            System.out.println("转账成功!");
        }
    }
}

2.1.2 トランザクションの導入

この場合の転送には 2 つの DML 操作 (1 つは送信、もう 1 つは受信) が含まれます。これらの操作はアトミックで分割不可能である必要があります。そうしないと、データ変更例外が発生する可能性があります。

public class AccountService {
    
    
    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();
        try {
    
    
            // 转出
            ad.out(outUser, money);
            // 模拟转账过程中的异常
            int i = 1/0;
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

したがって、ここでは、転送と転送の操作がアトミックであり、同時に成功するか同時に失敗するようにトランザクションを操作する必要があります。

(1) JDBCにおけるトランザクション操作用のAPI

接続インターフェースのメソッド 効果
void setAutoCommit(false) 自動トランザクションコミットを無効にする (代わりに手動)
ボイドコミット(); トランザクションをコミットする
ボイドロールバック(); トランザクションのロールバック

(2) 取引開始時の注意点

  • すべての操作が 1 つのトランザクション内で行われるようにするには、このケースで使用される接続が同じである必要があります。トランザクションを開くためのサービス層の接続は、データベースにアクセスするための dao 層の接続と一致している必要があります。

  • スレッドの同時実行の場合、各スレッドは独自の接続のみを操作できます。

2.2 従来のソリューション

2.2.1 従来方式の実現

上記の前提に基づいて、通常考えられる解決策は次のとおりです。

  • パラメータの受け渡し: 接続オブジェクトをサービス層から dao 層に渡します。
  • ロック

コード実装の変更部分は次のとおりです。

(1) AccountServiceクラス

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {
    
    

    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();
        //线程并发情况下,为了保证每个线程使用各自的connection,故加锁
        synchronized (AccountService.class) {
    
    

            Connection conn = null;
            try {
    
    
                conn = JdbcUtils.getConnection();
                //开启事务
                conn.setAutoCommit(false);
                // 转出
                ad.out(conn, outUser, money);
                // 模拟转账过程中的异常
//            int i = 1/0;
                // 转入
                ad.in(conn, inUser, money);
                //事务提交
                JdbcUtils.commitAndClose(conn);
            } catch (Exception e) {
    
    
                e.printStackTrace();
                //事务回滚
                JdbcUtils.rollbackAndClose(conn);
                return false;
            }
            return true;
        }
    }
}

(2) AccountDao クラス (ここで注意すべき点は、接続はdao層では解放できませんが、サービス層では解放されます。そうでないとdao層で解放され、サービス層が利用できなくなります)

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {
    
    

    public void out(Connection conn, String outUser, int money) throws SQLException{
    
    
        String sql = "update account set money = money - ? where name = ?";
        //注释从连接池获取连接的代码,使用从service中传递过来的connection
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //连接不能在这里释放,service层中还需要使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(Connection conn, String inUser, int money) throws SQLException {
    
    
        String sql = "update account set money = money + ? where name = ?";
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

2.2.2 従来方式のデメリット

上記の方法で実際に必要に応じて問題が解決されたことがわかりますが、注意深く観察すると、この実装には欠点があることがわかります。

  1. 接続をサービス層から dao 層に直接渡すため、コード結合が増加します。

  2. ロックするとスレッドの同時実行性が失われ、プログラムのパフォーマンスが低下します。

2.3 ThreadLocal ソリューション

2.3.1 ThreadLocal スキームの実装

プロジェクト内でデータ転送スレッド分離が必要なこのようなシナリオの場合は、ThreadLocal を使用して解決することもできます。

(1) ツールクラスの修正:ThreadLocalの追加

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    
    
    //ThreadLocal对象 : 将connection绑定在当前线程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();

    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    // 获取连接
    public static Connection getConnection() throws SQLException {
    
    
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
    
    
            //如果没有,则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中
            tl.set(conn);
        }
        return conn;
    }

    //释放资源
    public static void release(AutoCloseable... ios) {
    
    
        for (AutoCloseable io : ios) {
    
    
            if (io != null) {
    
    
                try {
    
    
                    io.close();
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

    public static void commitAndClose() {
    
    
        try {
    
    
            Connection conn = getConnection();
            //提交事务
            conn.commit();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose() {
    
    
        try {
    
    
            Connection conn = getConnection();
            //回滚事务
            conn.rollback();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
    }
}

(2) AccountServiceクラスの変更:接続オブジェクトを渡す必要はありません

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {
    
    

    public boolean transfer(String outUser, String inUser, int money) {
    
    
        AccountDao ad = new AccountDao();

        try {
    
    
            Connection conn = JdbcUtils.getConnection();
            //开启事务
            conn.setAutoCommit(false);
            // 转出 : 这里不需要传参了 !
            ad.out(outUser, money);
            // 模拟转账过程中的异常
//            int i = 1 / 0;
            // 转入
            ad.in(inUser, money);
            //事务提交
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            //事务回滚
           JdbcUtils.rollbackAndClose();
            return false;
        }
        return true;
    }
}

(3) AccountDaoクラスの修正:通常通り使用

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {
    
    

    public void out(String outUser, int money) throws SQLException {
    
    
        String sql = "update account set money = money - ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //照常使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(String inUser, int money) throws SQLException {
    
    
        String sql = "update account set money = money + ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

2.3.2 ThreadLocal スキームの利点

上記のケースから、特定のシナリオでは、ThreadLocal ソリューションには 2 つの優れた利点があることがわかります。

  1. データの転送: 各スレッドによってバインドされたデータを保存し、必要に応じて直接取得し、パラメータの直接転送によって引き起こされるコード結合の問題を回避します。

  2. スレッド分離: 各スレッド間のデータは相互に分離されていますが、同時実行性があり、同期によるパフォーマンスの低下を回避します。

3. ThreadLocal の内部構造

上記の検討を通じて、ThreadLocal の役割について一定の理解が得られました。次に、ThreadLocal の内部構造を見て、スレッド データの分離をどのように実現できるかを見てみましょう。

3.1 よくある誤解

ソースコードを見なければ、ThreadLocal次のように設計されていると推測できます。ThreadLocalそれぞれに 1 つずつ作成しMapスレッドをローカル変数として使用して保存しますこれにより、各スレッドのローカル変数を保存できます。孤立した効果。これは最も単純な設計方法であり、初期の JDK は確かにこのように設計されていましたが、現在はそうではありません。MapkeyMapvalueThreadLocal

1582788729557

3.2 現在の設計

しかし、JDK は後に設計スキームを最適化し、ThreadLocalJDK8 の設計は、それぞれがThread1 つを保持しThreadLocalMap、この Mapがインスタンスそのものでkeyあり、これが実際に格納される値ですThreadLocalvalueObject

具体的なプロセスは次のとおりです。

(1) 各 Thread スレッドの内部には Map (ThreadLocalMap) があります
(2) Map には ThreadLocal オブジェクト (キー) とスレッドの変数コピー (値) が格納されます
(3) Thread 内の Map は ThreadLocal によって維持され、ThreadLocal はマップするスレッド変数値の取得と設定を担当します。
(4) 異なるスレッドの場合、コピー値が取得されるたびに、他のスレッドは現在のスレッドのコピー値を取得できなくなり、コピーの分離が形成され、相互に干渉しなくなります。

1574155226793

3.3 この設計の利点

この設計は冒頭で述べたこととは全く逆で、次の 2 つの利点があります。

(1) この設計以降、Map各ストレージの数はEntry削減されます。以前の保管数量はThread数量によって決定されていたため、現在はThreadLocal数量によって決定されます。実際の使用では、ThreadLocal の数はスレッドの数よりも少ないことがよくあります。

(2)Thread破壊すると対応するものもThreadLocalMap破壊されるため、メモリの使用量を削減できます。

4. ThreadLocal のコアメソッドのソースコード

ThreadLocal の内部構造に基づいて、その動作原理をより深く理解するために、そのコア メソッドのソース コードの分析を続けています。

ThreadLocal には構築メソッドに加えて、次の 4 つのメソッドが外部に公開されています。

メソッド宣言 説明する
protected T 初期値() 現在のスレッドローカル変数の初期値を返します。
public void set(T 値) 現在のスレッドにバインドされたローカル変数を設定します
public T get() 現在のスレッドによってバインドされているローカル変数を取得します
パブリック void 削除() 現在のスレッドによってバインドされているローカル変数を削除します

以下は、これら 4 つのメソッドの詳細なソース コード分析です (明確なアイデアを確保するために、当面は ThreadLocalMap 部分は拡張されず、次のナレッジ ポイントで詳細に説明されます)

4.1 セット方法

(1) ソースコードと対応する中国語コメント

  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
    
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
    
    
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
    
    
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2) コード実行処理

A. まず現在のスレッドを取得し、現在のスレッドに基づいてマップを取得します。

B. 取得した Map が空でない場合は、パラメータを Map に設定します (現在の ThreadLocal 参照がキーとして使用されます)。

C. マップが空の場合は、スレッドのマップを作成し、初期値を設定します

4.2 getメソッド

(1) ソースコードと対応する中国語コメント

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
    
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
    
    
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
    
    
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
    
    
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

(2) コード実行処理

A. まず現在のスレッドを取得し、現在のスレッドに基づいてマップを取得します。

B. 取得したマップが空でない場合は、ThreadLocal 参照をマップ内のキーとして使用して、マップ内の対応するエントリ e を取得します。それ以外の場合は、D に進みます。

C. e が null でない場合は e.value を返し、それ以外の場合は D に進みます。

D. マップが空であるか e が空の場合、initialValue 関数を使用して初期値の値を取得し、ThreadLocal 参照と値を firstKey および firstValue として使用して新しいマップを作成します

概要:まず現在のスレッドの ThreadLocalMap 変数を取得し、値が存在する場合はその値を返し、存在しない場合は初期値を作成して返します。

4.3 削除メソッド

(1) ソースコードと対応する中国語コメント

 /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
    
    
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

(2) コード実行処理

A. まず現在のスレッドを取得し、現在のスレッドに基づいてマップを取得します。

B. 取得した Map が空でない場合は、現在の ThreadLocal オブジェクトに対応するエントリを削除します。

4.4 initialValue方法

/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    
    
    return null;
}

このメソッドの機能は、スレッド ローカル変数の初期値を返すことです。

(1) このメソッドは遅延呼び出しメソッドであり、上記のコードから、set メソッドが呼び出される前に get メソッドが呼び出されたときに実行され、一度だけ実行されることがわかります。

(2) このメソッドのデフォルト実装は、直接 1 を返しますnull

(3) null 以外の初期値が必要な場合は、このメソッドをオーバーライドできます。(注: このメソッドはprotected独自のメソッドであり、明らかにサブクラスによってオーバーライドされるように設計されています)

5. ThreadLocalMap のソースコード分析

ThreadLocal メソッドを分析すると、ThreadLocal の操作は実際には ThreadLocalMap を中心に展開していることがわかりました。ThreadLocalMap のソースコードは比較的複雑ですが、以下の 3 つの側面から説明します。

5.1 基本構造

ThreadLocalMap は ThreadLocal の内部クラスであり、Map インターフェースを実装せず、Map の機能を独自に実装し、内部の Entry も独自に実装します。

1574266262577

(1) メンバ変数

    /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0
    

HashMap と同様に、INITIAL_CAPACITY はマップの初期容量を表します。table はデータの保存に使用される Entry タイプの配列です。size はテーブル内のストレージの数を表します。threshold は拡張が必要な​​場合のサイズに対応するしきい値を表します。

リンク: https://www.jianshu.com/p/acfd2239c9f4

出典:建書

著作権は作者に帰属します。商業的転載の場合は著者に連絡して承認を求め、非商業的転載の場合は出典を明記してください。

(2) ストレージ構造 - エントリ

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
    /** The value associated with this ThreadLocal. */
    Object value;

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

ThreadLocalMap では、Entry は KV 構造データの保存にも使用されます。ただし、Entry のキーは ThreadLocal オブジェクトのみにすることができ、構築メソッドで制限されています。

さらに、Entry は WeakReference を継承します。つまり、キー (ThreadLocal) は弱参照であり、その目的は、ThreadLocal オブジェクトのライフ サイクルとスレッドのライフ サイクルのバインドを解除することです。

5.2 弱い参照とメモリ リーク

プログラマの中には、ThreadLocal を使用するプロセスでメモリ リークがあることに気づき、このメモリ リークが Entry で弱い参照を使用するキーに関連していると推測する人もいます。この理解は実は間違っています。

まず、この質問に含まれるいくつかの名詞の概念を確認してから、問題を分析しましょう。

(1) メモリリークに関する概念

  • メモリ オーバーフロー: メモリ オーバーフロー。申請者が使用できる十分なメモリがありません。
  • メモリ リーク: メモリ リークとは、プログラム内で動的に割り当てられたヒープ メモリが解放されないか、何らかの理由で解放できないことを指します。その結果、システム メモリが無駄に消費され、システムの実行速度が低下するなどの重大な結果が生じます。プログラムがクラッシュしたり、システムがクラッシュしたりすることもあります。メモリ リークが蓄積すると、最終的にはメモリ オーバーフローが発生します。

(2) 関連概念への言及が弱い

Java には、強い参照、ソフト参照、弱い参照、仮想参照の 4 種類の参照があります。現在の問題は主に強参照と弱参照に関係しています。

強い参照 (「強い」参照) は、最も一般的な一般的なオブジェクト参照です。オブジェクトを指す強い参照がある限り、オブジェクトがまだ「生きている」ことを示すことができ、ガベージ コレクターはリサイクルしません。このオブジェクト。

弱い参照 (WeakReference)では、ガベージ コレクターが弱い参照のみを持つオブジェクトを見つけると、現在のメモリ領域が十分であるかどうかに関係なく、そのメモリを再利用します。

(3) キーが強参照を使用している場合

ThreadLocalMap のキーが強参照を使用すると仮定すると、メモリ リークは発生しますか?

このときの ThreadLocal のメモリマップ(実線は強参照)は以下の通りです。

1582902708234

ビジネスコードで ThreadLocal が使用されると仮定すると、threadLocal Ref が再利用されます。

ただし、threadLocalMap のエントリは threadLocal を強く参照しているため、threadLocal をリサイクルすることはできません。

エントリが手動で削除されておらず、CurrentThread がまだ実行中であるという前提の下では、常に threadRef->currentThread->threadLocalMap->entry という強力な参照チェーンが存在し、エントリはリサイクルされません (ThreadLocal インスタンスと値はエントリに含まれている)、エントリのメモリ リークを引き起こします。

つまり、ThreadLocalMap のキーは強参照を使用しているため、メモリ リークを完全に回避することはできません。

(5) キーが弱い参照を使用している場合

次に、ThreadLocalMap のキーは弱い参照を使用しますが、メモリ リークは発生しますか?

このときの ThreadLocal のメモリマップ(実線は強参照、点線は弱参照)は以下の通りです。

1582907143471

また、ビジネス コードで ThreadLocal を使用した後、ThreadLocal Ref がリサイクルされることも想定されています。

ThreadLocalMap は ThreadLocal への弱参照のみを保持し、threadlocal インスタンスを指す強参照がないため、gc によって threadlocal を正常にリサイクルできます。このとき、Entry では key=null になります。

ただし、エントリが手動で削除されておらず、CurrentThread がまだ実行中であるという前提の下では、threadRef->currentThread->threadLocalMap->entry -> value という強力な参照チェーンもあり、値はリサイクルされず、この値はアクセスに達すると、値のメモリ リークが発生します。

つまり、ThreadLocalMap のキーは弱い参照を使用しており、メモリ リークが発生する可能性があります。

(6) メモリリークの本当の理由

上記 2 つの状況を比較すると、メモリ リークの発生は、ThreadLocalMap のキーが弱い参照を使用しているかどうかとは関係がないことがわかります。では、メモリリークの本当の原因は何でしょうか?

注意深い学生であれば、上記の 2 つのメモリ リークの状況には、次の 2 つの前提条件があることがわかるでしょう。

1. 没有手动删除这个Entry
2. CurrentThread依然运行

1点目はわかりやすいですが、ThreadLocalを使用し、そのremoveメソッドを呼び出して対応するEntryを削除する限り、メモリリークは回避できます。

2 番目の点は少し複雑ですが、ThreadLocalMap は Thread の属性であり、現在のスレッドによって参照されるため、そのライフサイクルは Thread と同じ長さになります。ThreadLocal を使用した後、現在の Thread も終了すると、ThreadLocalMap は gc によって自然にリサイクルされ、根本原因によるメモリ リークが回避されます。

要約すると、ThreadLocal メモリ リークの根本原因は次のとおりです。 ThreadLocalMap のライフサイクルは Thread と同じくらい長いため、対応するキーを手動で削除しないとメモリ リークが発生します。

(7) 弱い参照を使用する理由

先ほどの分析によると、ThreadLocalMap のキーがどのような種類の参照を使用しても、メモリ リークを完全に回避することはできず、弱い参照の使用とは何の関係もないことがわかりました。

メモリ リークを回避するには 2 つの方法があります。

  1. ThreadLocal を使用した後、remove メソッドを呼び出して、対応するエントリを削除します。

  2. ThreadLocal を使用すると、現在のスレッドも終了します

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

つまり、ThreadLocal を使用した後、時間内に Remove を呼び出すことを覚えていれば、キーが強参照であっても弱参照であっても問題はありません。では、なぜキーは弱い参照を使用するのでしょうか?

実際、ThreadLocalMapのset/getEntryメソッドでは、キーがnull(つまりThreadLocalがnull)と判定され、nullの場合は値がnullに設定されます。

これは、ThreadLocal を使用した後、CurrentThread がまだ実行中であることを意味します。remove メソッドを呼び出すのを忘れた場合でも、弱参照は強参照よりも保護層を 1 つ多く提供できます。弱参照された ThreadLocal はリサイクルされ、対応する値は再利用されます。は、次回の ThreadLocalMap 呼び出しで設定されます。メモリ リークを避けるために、get または Remove のメソッドはすべてクリアされます。

5.3 ハッシュ競合の解決

ハッシュ競合の解決は、Map の重要なコンテンツです。ハッシュ競合の解決を手がかりにして、ThreadLocalMap のコア ソース コードを調べてみましょう。

(1) まずはThreadLocalのset()メソッドから始めます

  public void set(T value) {
    
    
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用了ThreadLocalMap的set方法
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
    
    
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
    
    
        	//调用了ThreadLocalMap的构造方法
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

このメソッドを今分析しました。その機能は、現在のスレッドによってバインドされているローカル変数を設定することです。

A. まず現在のスレッドを取得し、現在のスレッドに基づいてマップを取得します。

B. 取得した Map が空でない場合は、パラメータを Map に設定します (現在の ThreadLocal 参照がキーとして使用されます)。

(ここで ThreadLocalMap の set メソッドが呼び出されます)

C. マップが空の場合は、スレッドのマップを作成し、初期値を設定します

(ここで ThreadLocalMap のコンストラクターが呼び出されます)

このコードには、ThreadLocalMap の 2 つのメソッドがそれぞれ関与する箇所が 2 か所あります。次に、これら 2 つのメソッドを分析します。

**(2) 構築メソッド`ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)**

 /*
  * firstKey : 本ThreadLocal实例(this)
  * firstValue : 要保存的线程本地变量
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引(重点代码)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //设置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //设置阈值
        setThreshold(INITIAL_CAPACITY);
    }

コンストラクターはまず長さ 16 の Entry 配列を作成し、次に firstKey に対応するインデックスを計算してテーブルに格納し、サイズとしきい値を設定します。

主要な分析: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

a. 以下に関してfirstKey.threadLocalHashCode

 	private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
    
    
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647;

ここでは AtomicInteger 型が定義されており、現在の値が取得されて HASH_INCREMENT が追加されるたびに、HASH_INCREMENT = 0x61c88647この値がフィボナッチ数列 (黄金分割数) に関連付けられ、その主な目的はハッシュ コードを 2 に均等に分散させることです。 n 乗配列、つまり Entry[] テーブル内でそうすることで、ハッシュの競合を可能な限り回避できます。

b. 概要& (INITIAL_CAPACITY - 1)

hashCode & (size - 1) アルゴリズムはハッシュの計算に使用されます。これは、モジュロ演算 hashCode % size のより効率的な実装と同等です。サイズが 2 の整数乗である必要があるのはまさにこのアルゴリズムのためであり、インデックスが境界を越えないという前提でハッシュ衝突の数を確実に減らすこともできます。

(3) ThreadLocalMapのsetメソッド

private void set(ThreadLocal<?> key, Object value) {
    
    
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
    
    
            ThreadLocal<?> k = e.get();
            //ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
    
    
                e.value = value;
                return;
            }
            // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
           // 当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
    
    
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots用于清除那些e.get()==null的元素,
             * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
             * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 * rehash(执行一次全表的扫描清理工作)
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
    
    
        return ((i + 1 < len) ? i + 1 : 0);
    }

コード実行フロー:

A. まず、キーに従ってインデックス i を計算し、次に i の位置にあるエントリを見つけます。

B. エントリがすでに存在し、キーが受信キーと等しい場合は、この時点で新しい値をエントリに直接割り当てます。

C. エントリは存在するが、キーが null の場合は、replaceStaleEntry を呼び出して、キーが空のエントリを置き換えます。

D. null の箇所が見つかるまで検出をループし続ける このとき、ループ処理中に戻ってこなかった場合は、null の位置に新しい Entry を作成して挿入すると、同時にサイズが 1 増加します時間。

最後に cleanSomeSlots を呼び出してキーが null の Entry をクリーンアップし、最後に Entry がクリーンアップされたかどうかを返し、sz >= thresgold が再ハッシュ条件を満たしているかどうかを判断します。満たしている場合は、再ハッシュ関数が呼び出されて完全なハッシュが実行されます。テーブルのスキャンとクリーンアップ。

主要な分析: ThreadLocalMap は线性探测法ハッシュの競合を解決するために使用されます。

この方法は、次のアドレスを一度に検出し、空きアドレスがなくなるまで挿入していく方式ですが、空間全体に空きアドレスがなくなるとオーバーフローが発生します。

たとえば、現在のテーブルの長さが 16 であるとします。つまり、計算されたキーのハッシュ値が 14 で、table[14] にすでに値があり、そのキーが現在のキーと一致しない場合、このとき、14に1を加えて15とし、table[15]で判定しますが、それでも競合があれば0に戻してtable[0]を取得し、挿入できるまで続きます。

上記の説明によれば、Entry[] テーブルは循環配列とみなすことができます。

おすすめ

転載: blog.csdn.net/m0_47015897/article/details/132260978