中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

ThreadLocal使用案例_動力節點Java學院整理

發布時間:2020-10-11 16:31:22 來源:腳本之家 閱讀:121 作者:mrr 欄目:編程語言

用戶提出一個需求:當修改產品價格的時候,需要記錄操作日志,什么時候做了什么事情。

想必這個案例,只要是做過應用系統的小伙伴們,都應該遇到過吧?無外乎數據庫里就兩張表:product 與 log,用兩條 SQL 語句應該可以解決問題:

update product set price = ? where id = ?
insert into log (created, description) values (?, ?)

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 也提高了性能啊。怎么搞呢?

原來要使每個線程都擁有自己的連接,而不是共享同一個連接,否則線程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!

我去!總算是解決了,QA 妹妹,你應該會對我微笑一下吧?

感謝您的關注,分享是一種快樂,也希望得到您的支持與批評!

注意:該示例僅用于說明 TheadLocal 的基本用法。在實際工作中,推薦使用連接池來管理數據庫連接。示例中的代碼僅作參考,使用前請酌情考慮。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

台北县| 吉隆县| 海南省| 抚远县| 明水县| 曲阜市| 白河县| 明光市| 休宁县| 巴青县| 肃北| 张家口市| 康保县| 松江区| 雷山县| 瑞金市| 宁远县| 印江| 桂林市| 三江| 遂川县| 开平市| 白沙| 梁平县| 华亭县| 河西区| 鹤山市| 左权县| 类乌齐县| 天台县| 蒲江县| 永平县| 汉川市| 遵化市| 翁源县| 沭阳县| 宜春市| 桃江县| 洞口县| 南丹县| 武夷山市|