国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術(shù)|正則表達(dá)式|C/C++|IOS|C#|Swift|Android|VB|R語(yǔ)言|JavaScript|易語(yǔ)言|vb.net|

服務(wù)器之家 - 編程語(yǔ)言 - Java教程 - Java多線程編程中ThreadLocal類的用法及深入

Java多線程編程中ThreadLocal類的用法及深入

2020-08-29 11:22黃勇 Java教程

這篇文章主要介紹了Java多線程編程中ThreadLocal類的用法及深入,嘗試了自己實(shí)現(xiàn)一個(gè)ThreadLocal類以及對(duì)相關(guān)的線程安全問題進(jìn)行討論,需要的朋友可以參考下

ThreadLocal,直譯為“線程本地”或“本地線程”,如果你真的這么認(rèn)為,那就錯(cuò)了!其實(shí),它就是一個(gè)容器,用于存放線程的局部變量,我認(rèn)為應(yīng)該叫做 ThreadLocalVariable(線程局部變量)才對(duì),真不理解為什么當(dāng)初 Sun 公司的工程師這樣命名。

早在 JDK 1.2 的時(shí)代,java.lang.ThreadLocal 就誕生了,它是為了解決多線程并發(fā)問題而設(shè)計(jì)的,只不過設(shè)計(jì)得有些難用,所以至今沒有得到廣泛使用。其實(shí)它還是挺有用的,不相信的話,我們一起來看看這個(gè)例子吧。

一個(gè)序列號(hào)生成器的程序,可能同時(shí)會(huì)有多個(gè)線程并發(fā)訪問它,要保證每個(gè)線程得到的序列號(hào)都是自增的,而不能相互干擾。

先定義一個(gè)接口:

?
1
2
3
4
public interface Sequence {
 
  int getNumber();
}

每次調(diào)用 getNumber() 方法可獲取一個(gè)序列號(hào),下次再調(diào)用時(shí),序列號(hào)會(huì)自增。

再做一個(gè)線程類:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ClientThread extends Thread {
 
  private Sequence sequence;
 
  public ClientThread(Sequence sequence) {
    this.sequence = sequence;
  }
 
  @Override
  public void run() {
    for (int i = 0; i < 3; i++) {
      System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());
    }
  }
 
 
 
}

 

在線程中連續(xù)輸出三次線程名與其對(duì)應(yīng)的序列號(hào)。

我們先不用 ThreadLocal,來做一個(gè)實(shí)現(xiàn)類吧。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SequenceA implements Sequence {
 
  private static int number = 0;
 
  public int getNumber() {
    number = number + 1;
    return number;
  }
 
  public static void main(String[] args) {
    Sequence sequence = new SequenceA();
 
    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);
 
    thread1.start();
    thread2.start();
    thread3.start();
  }
}

 

序列號(hào)初始值是0,在 main() 方法中模擬了三個(gè)線程,運(yùn)行后結(jié)果如下:

?
1
2
3
4
5
6
7
8
9
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 4
Thread-2 => 5
Thread-2 => 6
Thread-1 => 7
Thread-1 => 8
Thread-1 => 9

由于線程啟動(dòng)順序是隨機(jī)的,所以并不是0、1、2這樣的順序,這個(gè)好理解。為什么當(dāng) Thread-0 輸出了1、2、3之后,而 Thread-2 卻輸出了4、5、6呢?線程之間竟然共享了 static 變量!這就是所謂的“非線程安全”問題了。

那么如何來保證“線程安全”呢?對(duì)應(yīng)于這個(gè)案例,就是說不同的線程可擁有自己的 static 變量,如何實(shí)現(xiàn)呢?下面看看另外一個(gè)實(shí)現(xiàn)吧。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SequenceB implements Sequence {
 
  private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return 0;
    }
  };
 
  public int getNumber() {
    numberContainer.set(numberContainer.get() + 1);
    return numberContainer.get();
  }
 
  public static void main(String[] args) {
    Sequence sequence = new SequenceB();
 
    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);
 
    thread1.start();
    thread2.start();
    thread3.start();
  }
}

 

通過 ThreadLocal 封裝了一個(gè) Integer 類型的 numberContainer 靜態(tài)成員變量,并且初始值是0。再看 getNumber() 方法,首先從 numberContainer 中 get 出當(dāng)前的值,加1,隨后 set 到 numberContainer 中,最后將 numberContainer 中 get 出當(dāng)前的值并返回。

是不是很惡心?但是很強(qiáng)大!確實(shí)稍微饒了一下,我們不妨把 ThreadLocal 看成是一個(gè)容器,這樣理解就簡(jiǎn)單了。所以,這里故意用 Container 這個(gè)單詞作為后綴來命名 ThreadLocal 變量。

運(yùn)行結(jié)果如何呢?看看吧。

?
1
2
3
4
5
6
7
8
9
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3

每個(gè)線程相互獨(dú)立了,同樣是 static 變量,對(duì)于不同的線程而言,它沒有被共享,而是每個(gè)線程各一份,這樣也就保證了線程安全。 也就是說,TheadLocal 為每一個(gè)線程提供了一個(gè)獨(dú)立的副本!

搞清楚 ThreadLocal 的原理之后,有必要總結(jié)一下 ThreadLocal 的 API,其實(shí)很簡(jiǎn)單。

  • public void set(T value):將值放入線程局部變量中
  • public T get():從線程局部變量中獲取值
  • public void remove():從線程局部變量中移除值(有助于 JVM 垃圾回收)
  • protected T initialValue():返回線程局部變量中的初始值(默認(rèn)為 null)

為什么 initialValue() 方法是 protected 的呢?就是為了提醒程序員們,這個(gè)方法是要你們來實(shí)現(xiàn)的,請(qǐng)給這個(gè)線程局部變量一個(gè)初始值吧。

了解了原理與這些 API,其實(shí)想想 ThreadLocal 里面不就是封裝了一個(gè) Map 嗎?自己都可以寫一個(gè) ThreadLocal 了,嘗試一下吧。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThreadLocal<T> {
 
  private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());
 
  public void set(T value) {
    container.put(Thread.currentThread(), value);
  }
 
  public T get() {
    Thread thread = Thread.currentThread();
    T value = container.get(thread);
    if (value == null && !container.containsKey(thread)) {
      value = initialValue();
      container.put(thread, value);
    }
    return value;
  }
 
  public void remove() {
    container.remove(Thread.currentThread());
  }
 
  protected T initialValue() {
    return null;
  }
}

 

以上完全山寨了一個(gè) ThreadLocal,其中中定義了一個(gè)同步 Map(為什么要這樣?請(qǐng)讀者自行思考),代碼應(yīng)該非常容易讀懂。
下面用這 MyThreadLocal 再來實(shí)現(xiàn)一把看看。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SequenceC implements Sequence {
 
  private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return 0;
    }
  };
 
  public int getNumber() {
    numberContainer.set(numberContainer.get() + 1);
    return numberContainer.get();
  }
 
  public static void main(String[] args) {
    Sequence sequence = new SequenceC();
 
    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);
 
    thread1.start();
    thread2.start();
    thread3.start();
  }
}

 以上代碼其實(shí)就是將 ThreadLocal 替換成了 MyThreadLocal,僅此而已,運(yùn)行效果和之前的一樣,也是正確的。

其實(shí) ThreadLocal 可以單獨(dú)成為一種設(shè)計(jì)模式,就看你怎么看了。

ThreadLocal 具體有哪些使用案例呢?

我想首先要說的就是:通過 ThreadLocal 存放 JDBC Connection,以達(dá)到事務(wù)控制的能力。

還是保持我一貫的 Style,用一個(gè) Demo 來說話吧。用戶提出一個(gè)需求:當(dāng)修改產(chǎn)品價(jià)格的時(shí)候,需要記錄操作日志,什么時(shí)候做了什么事情。

想必這個(gè)案例,只要是做過應(yīng)用系統(tǒng)的小伙伴們,都應(yīng)該遇到過吧?無外乎數(shù)據(jù)庫(kù)里就兩張表:product 與 log,用兩條 SQL 語(yǔ)句應(yīng)該可以解決問題:

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

But!要確保這兩條 SQL 語(yǔ)句必須在同一個(gè)事務(wù)里進(jìn)行提交,否則有可能 update 提交了,但 insert 卻沒有提交。如果這樣的事情真的發(fā)生了,我們肯定會(huì)被用戶指著鼻子狂罵:“為什么產(chǎn)品價(jià)格改了,卻看不到什么時(shí)候改的呢?”。

聰明的我在接到這個(gè)需求以后,是這樣做的:

首先,我寫一個(gè) DBUtil 的工具類,封裝了數(shù)據(jù)庫(kù)的常用操作:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DBUtil {
  // 數(shù)據(jù)庫(kù)配置
  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";
 
  // 定義一個(gè)數(shù)據(jù)庫(kù)連接
  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;
  }
 
  // 關(guān)閉連接
  public static void closeConnection() {
    try {
      if (conn != null) {
        conn.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

里面搞了一個(gè) static 的 Connection,這下子數(shù)據(jù)庫(kù)連接就好操作了,牛逼吧!

然后,我定義了一個(gè)接口,用于給邏輯層來調(diào)用:

?
1
2
3
4
public interface ProductService {
 
  void updateProductPrice(long productId, int price);
}

根據(jù)用戶提出的需求,我想這個(gè)接口完全夠用了。根據(jù) productId 去更新對(duì)應(yīng) Product 的 price,然后再插入一條數(shù)據(jù)到 log 表中。

其實(shí)業(yè)務(wù)邏輯也不太復(fù)雜,于是我快速地完成了 ProductService 接口的實(shí)現(xiàn)類:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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); // 關(guān)閉自動(dòng)提交事務(wù)(開啟事務(wù))
 
      // 執(zhí)行操作
      updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新產(chǎn)品
      insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志
 
      // 提交事務(wù)
      conn.commit();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      // 關(guān)閉連接
      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!");
    }
  }
}

代碼的可讀性還算不錯(cuò)吧?這里我用到了 JDBC 的高級(jí)特性 Transaction 了。暗自慶幸了一番之后,我想是不是有必要寫一個(gè)客戶端,來測(cè)試一下執(zhí)行結(jié)果是不是我想要的呢? 于是我偷懶,直接在 ProductServiceImpl 中增加了一個(gè) main() 方法:

?
1
2
3
4
public static void main(String[] args) {
  ProductService productService = new ProductServiceImpl();
  productService.updateProductPrice(1, 3000);
}

我想讓 productId 為 1 的產(chǎn)品的價(jià)格修改為 3000。于是我把程序跑了一遍,控制臺(tái)輸出:

?
1
2
Update product success!
Insert log success!

應(yīng)該是對(duì)了。作為一名專業(yè)的程序員,為了萬(wàn)無一失,我一定要到數(shù)據(jù)庫(kù)里在看看。沒錯(cuò)!product 表對(duì)應(yīng)的記錄更新了,log 表也插入了一條記錄。這樣就可以將 ProductService 接口交付給別人來調(diào)用了。

幾個(gè)小時(shí)過去了,QA 妹妹開始罵我:“我靠!我才模擬了 10 個(gè)請(qǐng)求,你這個(gè)接口怎么就掛了?說是數(shù)據(jù)庫(kù)連接關(guān)閉了!”。

聽到這樣的叫聲,讓我渾身打顫,立馬中斷了我的小視頻,趕緊打開 IDE,找到了這個(gè) ProductServiceImpl 這個(gè)實(shí)現(xiàn)類。好像沒有 Bug 吧?但我現(xiàn)在不敢給她任何回應(yīng),我確實(shí)有點(diǎn)怕她的。

我突然想起,她是用工具模擬的,也就是模擬多個(gè)線程了!那我自己也可以模擬啊,于是我寫了一個(gè)線程類:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
  }
}

我用這線程去調(diào)用 ProduceService 的方法,看看是不是有問題。此時(shí),我還要再修改一下 main() 方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
// 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 個(gè)線程吧,我就不信那個(gè)邪了!

運(yùn)行結(jié)果真的讓我很暈、很暈:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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)

我靠!竟然在多線程的環(huán)境下報(bào)錯(cuò)了,果然是數(shù)據(jù)庫(kù)連接關(guān)閉了。怎么回事呢?我陷入了沉思中。于是我 Copy 了一把那句報(bào)錯(cuò)信息,在百度、Google,還有 OSC 里都找了,解答實(shí)在是千奇百怪。

我突然想起,既然是跟 Connection 有關(guān)系,那我就將主要精力放在檢查 Connection 相關(guān)的代碼上吧。是不是 Connection 不應(yīng)該是 static 的呢?我當(dāng)初設(shè)計(jì)成 static 的主要是為了讓 DBUtil 的 static 方法訪問起來更加方便,用 static 變量來存放 Connection 也提高了性能啊。怎么搞呢?

于是我看到了 OSC 上非常火爆的一篇文章《ThreadLocal 那點(diǎn)事兒》,終于才讓我明白了!原來要使每個(gè)線程都擁有自己的連接,而不是共享同一個(gè)連接,否則線程1有可能會(huì)關(guān)閉線程2的連接,所以線程2就報(bào)錯(cuò)了。一定是這樣!

我趕緊將 DBUtil 給重構(gòu)了:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DBUtil {
  // 數(shù)據(jù)庫(kù)配置
  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";
 
  // 定義一個(gè)用于放置數(shù)據(jù)庫(kù)連接的局部線程變量(使每個(gè)線程都擁有自己的連接)
  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;
  }
 
  // 關(guān)閉連接
  public static void closeConnection() {
    Connection conn = connContainer.get();
    try {
      if (conn != null) {
        conn.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      connContainer.remove();
    }
  }
}

我把 Connection 放到了 ThreadLocal 中,這樣每個(gè)線程之間就隔離了,不會(huì)相互干擾了。

此外,在 getConnection() 方法中,首先從 ThreadLocal 中(也就是 connContainer 中) 獲取 Connection,如果沒有,就通過 JDBC 來創(chuàng)建連接,最后再把創(chuàng)建好的連接放入這個(gè) ThreadLocal 中。可以把 ThreadLocal 看做是一個(gè)容器,一點(diǎn)不假。

同樣,我也對(duì) closeConnection() 方法做了重構(gòu),先從容器中獲取 Connection,拿到了就 close 掉,最后從容器中將其 remove 掉,以保持容器的清潔。

這下應(yīng)該行了吧?我再次運(yùn)行 main() 方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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!

總算是解決了

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 综合久| 国产操片| 成人精品一区二区 | 欧美日韩免费在线 | 午夜激情视频在线 | 中文字幕第33页 | 日韩视频在线观看一区 | 欧美激情精品久久久久 | 日韩精品毛片免费看 | 久久久精品久久久久 | 午夜精品久久久久久久久久久久 | 四虎网站| 日本黄色美女视频 | 欧美一级做a爰片久久高潮 免费在线毛片 | 欧美另类视频 | 久久xx| 久久久高清 | 国产精品日韩精品 | av在线免费观看网站 | 影音先锋男人网 | 国产目拍亚洲精品99久久精品 | 91精品国产日韩91久久久久久 | 午夜精品一区二区三区在线播放 | 三级视频网站 | 黄色毛片在线看 | 亚洲成人av| 久久视频国产 | 国产精品综合久久 | 在线看av的网址 | 国产激情一区二区三区成人免费 | 91精品一区二区三区久久久久久 | 黄色小网站免费观看 | 亚洲视频免费 | 中文字幕不卡一区 | 久草免费在线 | 日本激情视频一区二区三区 | 69免费视频 | 国产亚洲欧美在线 | 精品久久久久久久久久久 | 欧美亚洲国产一区二区三区 | 久操资源 |