Java マルチスレッド プログラミングにおける ThreadLocal クラスの使用法と詳細な使用法
ThreadLocal、直訳すると「スレッドローカル」または「ローカルスレッド」ですが、本当にそう思っているなら、それは間違いです。実際、これはスレッドのローカル変数を格納するために使用されるコンテナーであり、ThreadLocalVariable (スレッド ローカル変数) と呼ぶべきだと思います。なぜ Sun のエンジニアがこの名前を付けたのかはよくわかりません。
JDK 1.2 の時代には、java.lang.ThreadLocal が誕生し、マルチスレッドの同時実行の問題を解決するように設計されていましたが、使用が少し難しいように設計されていたため、まだ広く使用されていません。実際、これは非常に便利です。信じられない場合は、この例を見てみましょう。
シリアル番号生成プログラムには、複数のスレッドが同時にアクセスする可能性があり、各スレッドによって取得されるシリアル番号が増加し、相互に干渉しないようにする必要があります。
まずインターフェースを定義します:
getNumber() メソッドを呼び出すたびにシリアル番号を取得でき、次回呼び出すときにシリアル番号が自動的に増加します。
別のスレッドクラスを作成します:
スレッド名とそれに対応するシーケンス番号をスレッド内に連続して 3 回出力します。
まずはThreadLocalを使わずに実装クラスを作ってみましょう。
シーケンス番号の初期値は 0 です。 main() メソッドで 3 つのスレッドがシミュレートされます。実行後の結果は次のとおりです。
リーリーでは、「スレッドの安全性」を確保するにはどうすればよいでしょうか?この場合に対応して、異なるスレッドが独自の静的変数を持つことができることを意味します。これを実装するにはどうすればよいでしょうか。別の実装を見てみましょう。
リーリー
ThreadLocal は、Integer 型の numberContainer 静的メンバー変数をカプセル化し、初期値は 0 です。 getNumber() メソッドを見ると、まずnumberContainerから現在値を取得し、1を加算してnumberContainerに設定し、最後にnumberContainerから現在値を取得して返します。
嫌じゃないですか?しかし、とても強力です!確かに、理解が簡単になるように、少しの間、ThreadLocal をコンテナーとして考えることもできます。したがって、「Container」という単語は、ThreadLocal 変数に名前を付ける接尾語として意図的に使用されています。
結果はどうなりましたか?見てみましょう。
リーリー
ThreadLocal の原理を理解した後、ThreadLocal の API を要約する必要があります。これは実際には非常に簡単です。
- public void set(T value): スレッドのローカル変数に値を代入します
- public T get(): スレッドローカル変数から値を取得します
- public void Remove(): スレッドローカル変数から値を削除します (JVM ガベージコレクションに役立ちます)
- protected TInitialValue(): スレッドローカル変数の初期値を返します(デフォルトはnull)
原理とこれらの API を理解した後、実際に考えてみると、ThreadLocal は Map をカプセル化しているのではないでしょうか? ThreadLocal は自分で作成できるので、試してみてください。
リーリー
上記は、同期マップを定義する ThreadLocal の完全なコピーであり (なぜこれなのか? 自分で考えてください)、コードは非常に読みやすいはずです。
MyThreadLocal を使用して再度実装してみましょう。
実際、ThreadLocal は、見方によっては、それ自体がデザイン パターンになる可能性があります。
ThreadLocal の具体的な使用例は何ですか?
最初に言いたいのは、ThreadLocal を介して JDBC 接続を保存して、トランザクション制御機能を実現するということです。
いつものスタイルを維持して、デモ自体を見てみましょう。ユーザーは、商品の価格を変更する際に、いつ、何をしたかを操作ログとして記録する必要があるとの要望を出しました。
おそらくアプリケーションシステムを作ったことがある人なら誰でもこのケースに遭遇したことがあるのではないでしょうか?データベースには、product と log の 2 つのテーブルしかありません。2 つの SQL ステートメントを使用すると、問題が解決されます。
リーリー
But!要确保这两条 SQL 语句必须在同一个事务里进行提交,否则有可能 update 提交了,但 insert 却没有提交。如果这样的事情真的发生了,我们肯定会被用户指着鼻子狂骂:“为什么产品价格改了,却看不到什么时候改的呢?”。
聪明的我在接到这个需求以后,是这样做的:
首先,我写一个 DBUtil 的工具类,封装了数据库的常用操作:
public class DBUtil { // 数据库配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定义一个数据库连接 private static Connection conn = null; // 获取连接 public static Connection getConnection() { try { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } catch (Exception e) { e.printStackTrace(); } return conn; } // 关闭连接 public static void closeConnection() { try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } } }
里面搞了一个 static 的 Connection,这下子数据库连接就好操作了,牛逼吧!
然后,我定义了一个接口,用于给逻辑层来调用:
public interface ProductService { void updateProductPrice(long productId, int price); }
根据用户提出的需求,我想这个接口完全够用了。根据 productId 去更新对应 Product 的 price,然后再插入一条数据到 log 表中。
其实业务逻辑也不太复杂,于是我快速地完成了 ProductService 接口的实现类:
public class ProductServiceImpl implements ProductService { private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?"; private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)"; public void updateProductPrice(long productId, int price) { try { // 获取连接 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务(开启事务) // 执行操作 updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新产品 insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志 // 提交事务 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭连接 DBUtil.closeConnection(); } } private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception { PreparedStatement pstmt = conn.prepareStatement(updateProductSQL); pstmt.setInt(1, productPrice); pstmt.setLong(2, productId); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Update product success!"); } } private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception { PreparedStatement pstmt = conn.prepareStatement(insertLogSQL); pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())); pstmt.setString(2, logDescription); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Insert log success!"); } } }
代码的可读性还算不错吧?这里我用到了 JDBC 的高级特性 Transaction 了。暗自庆幸了一番之后,我想是不是有必要写一个客户端,来测试一下执行结果是不是我想要的呢? 于是我偷懒,直接在 ProductServiceImpl 中增加了一个 main() 方法:
public static void main(String[] args) { ProductService productService = new ProductServiceImpl(); productService.updateProductPrice(1, 3000); }
我想让 productId 为 1 的产品的价格修改为 3000。于是我把程序跑了一遍,控制台输出:
Update product success! Insert log success!
应该是对了。作为一名专业的程序员,为了万无一失,我一定要到数据库里在看看。没错!product 表对应的记录更新了,log 表也插入了一条记录。这样就可以将 ProductService 接口交付给别人来调用了。
几个小时过去了,QA 妹妹开始骂我:“我靠!我才模拟了 10 个请求,你这个接口怎么就挂了?说是数据库连接关闭了!”。
听到这样的叫声,让我浑身打颤,立马中断了我的小视频,赶紧打开 IDE,找到了这个 ProductServiceImpl 这个实现类。好像没有 Bug 吧?但我现在不敢给她任何回应,我确实有点怕她的。
我突然想起,她是用工具模拟的,也就是模拟多个线程了!那我自己也可以模拟啊,于是我写了一个线程类:
public class ClientThread extends Thread { private ProductService productService; public ClientThread(ProductService productService) { this.productService = productService; } @Override public void run() { System.out.println(Thread.currentThread().getName()); productService.updateProductPrice(1, 3000); } }
我用这线程去调用 ProduceService 的方法,看看是不是有问题。此时,我还要再修改一下 main() 方法:
// public static void main(String[] args) { // ProductService productService = new ProductServiceImpl(); // productService.updateProductPrice(1, 3000); // } public static void main(String[] args) { for (int i = 0; i < 10; i++) { ProductService productService = new ProductServiceImpl(); ClientThread thread = new ClientThread(productService); thread.start(); } }
我也模拟 10 个线程吧,我就不信那个邪了!
运行结果真的让我很晕、很晕:
Thread-1 Thread-3 Thread-5 Thread-7 Thread-9 Thread-0 Thread-2 Thread-4 Thread-6 Thread-8 Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) at java.lang.reflect.Constructor.newInstance(Constructor.java:513) at com.mysql.jdbc.Util.handleNewInstance(Util.java:411) at com.mysql.jdbc.Util.getInstance(Util.java:386) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920) at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304) at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296) at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699) at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25) at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)
我靠!竟然在多线程的环境下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。于是我 Copy 了一把那句报错信息,在百度、Google,还有 OSC 里都找了,解答实在是千奇百怪。
我突然想起,既然是跟 Connection 有关系,那我就将主要精力放在检查 Connection 相关的代码上吧。是不是 Connection 不应该是 static 的呢?我当初设计成 static 的主要是为了让 DBUtil 的 static 方法访问起来更加方便,用 static 变量来存放 Connection 也提高了性能啊。怎么搞呢?
于是我看到了 OSC 上非常火爆的一篇文章《ThreadLocal 那点事儿》,终于才让我明白了!原来要使每个线程都拥有自己的连接,而不是共享同一个连接,否则线程1有可能会关闭线程2的连接,所以线程2就报错了。一定是这样!
我赶紧将 DBUtil 给重构了:
public class DBUtil { // 数据库配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接) private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); // 获取连接 public static Connection getConnection() { Connection conn = connContainer.get(); try { if (conn == null) { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.set(conn); } return conn; } // 关闭连接 public static void closeConnection() { Connection conn = connContainer.get(); try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.remove(); } } }
我把 Connection 放到了 ThreadLocal 中,这样每个线程之间就隔离了,不会相互干扰了。
此外,在 getConnection() 方法中,首先从 ThreadLocal 中(也就是 connContainer 中) 获取 Connection,如果没有,就通过 JDBC 来创建连接,最后再把创建好的连接放入这个 ThreadLocal 中。可以把 ThreadLocal 看做是一个容器,一点不假。
同样,我也对 closeConnection() 方法做了重构,先从容器中获取 Connection,拿到了就 close 掉,最后从容器中将其 remove 掉,以保持容器的清洁。
这下应该行了吧?我再次运行 main() 方法:
Thread-0 Thread-2 Thread-4 Thread-6 Thread-8 Thread-1 Thread-3 Thread-5 Thread-7 Thread-9 Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success! Update product success! Insert log success!
总算是解决了

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック









Java の Weka へのガイド。ここでは、weka java の概要、使い方、プラットフォームの種類、利点について例を交えて説明します。

この記事では、Java Spring の面接で最もよく聞かれる質問とその詳細な回答をまとめました。面接を突破できるように。

Java 8は、Stream APIを導入し、データ収集を処理する強力で表現力のある方法を提供します。ただし、ストリームを使用する際の一般的な質問は次のとおりです。 従来のループにより、早期の中断やリターンが可能になりますが、StreamのForeachメソッドはこの方法を直接サポートしていません。この記事では、理由を説明し、ストリーム処理システムに早期終了を実装するための代替方法を調査します。 さらに読み取り:JavaストリームAPIの改善 ストリームを理解してください Foreachメソッドは、ストリーム内の各要素で1つの操作を実行する端末操作です。その設計意図はです

Java での日付までのタイムスタンプに関するガイド。ここでは、Java でタイムスタンプを日付に変換する方法とその概要について、例とともに説明します。

カプセルは3次元の幾何学的図形で、両端にシリンダーと半球で構成されています。カプセルの体積は、シリンダーの体積と両端に半球の体積を追加することで計算できます。このチュートリアルでは、さまざまな方法を使用して、Javaの特定のカプセルの体積を計算する方法について説明します。 カプセルボリュームフォーミュラ カプセルボリュームの式は次のとおりです。 カプセル体積=円筒形の体積2つの半球体積 で、 R:半球の半径。 H:シリンダーの高さ(半球を除く)。 例1 入力 RADIUS = 5ユニット 高さ= 10単位 出力 ボリューム= 1570.8立方ユニット 説明する 式を使用してボリュームを計算します。 ボリューム=π×R2×H(4

Java は、初心者と経験豊富な開発者の両方が学習できる人気のあるプログラミング言語です。このチュートリアルは基本的な概念から始まり、高度なトピックに進みます。 Java Development Kit をインストールしたら、簡単な「Hello, World!」プログラムを作成してプログラミングを練習できます。コードを理解したら、コマンド プロンプトを使用してプログラムをコンパイルして実行すると、コンソールに「Hello, World!」と出力されます。 Java の学習はプログラミングの旅の始まりであり、習熟が深まるにつれて、より複雑なアプリケーションを作成できるようになります。
