'小白:都說StringBuilder線程不安全,你為啥不用StringBuffer'

Java SQL 技術 互聯網活化石 2019-08-11
"

今天,一個小白跟我說,“我剛看了套Java面試題目,終於想清楚一個道理了,裡面說StringBuilder是線程不安全的,StringBuffer線程安全,難怪我們技術經理讓我們在寫業務代碼的時候要用StringBuffer。”

"

今天,一個小白跟我說,“我剛看了套Java面試題目,終於想清楚一個道理了,裡面說StringBuilder是線程不安全的,StringBuffer線程安全,難怪我們技術經理讓我們在寫業務代碼的時候要用StringBuffer。”

小白:都說StringBuilder線程不安全,你為啥不用StringBuffer

無語!

我一口老血噴出!這技術經理是走後門當上的吧。StringBuilder線程不安全,所以凡是用到字符串拼接的地方就改用StringBuffer?亂彈琴!知道Java裡存在線程安全,嗯,算是入門了,但是最基礎的問題要搞清楚,那就是線程安全什麼情況下會發生,什麼情況下才需要考慮,這是重點的重點。

方法內每次new的對象不存在線程安全問題

我們來看這段示例代碼:

@Service
public class UserService {
\t@Autowired
\tprotected BaseDAO<User> dao;
\t@Autowired
\tprivate UserDAO userDAO;
\tpublic boolean existUserByName(String userName,Integer sort) {
\t\tStringBuilder sbuilder = new StringBuilder("SELECT count(*) FROM {user} WHERE ";
sbuilder.append("username='");
sbuilder.append(userName);
sbuilder.append("' order by ");
sbuilder.append(sort);
\t\tint i = userDAO.queryForInt(sbuilder.toString());
\t\treturn i > 0;
\t}
}

我們先不關注拼接SQL是否規範,重點關注這裡的StringBuilder對象有沒有線程安全問題。

小白的邏輯是,這段業務代碼,在併發比較大的時候,會有線程安全問題。那事實呢?

要搞清楚的是雖然existUserByName會被多個線程同時執行到,但是做法方法裡的變量,sbuilder的作用域只存在於方法內部,並且每次都是new出來的對象。也就是說,一個Java Web的服務,每次請求進來都會對應一個線程,但是每個線程都new了自己的數據,所以這樣是不存在多線程問題的,況且userName這個參數也是外部傳入的,每一次都不一樣。

小白又說了,在Spring裡,所有的bean都默認是單例的,你說sbuilder每次都是new的,沒有線程安全問題,那麼userDAO這個對象,作為單例,所有線程共用這一個對象,有沒有安全問題?

無狀態變量不存在線程安全問題

我們來看這個userDAO對象,沒錯,它是單例,而且被共用,難道也不存在線程安全問題?

@Repository
@SqlResource("user")
public interface UserDao extends BaseMapper<User> {
int getCountWithName(@Param("name") String name);
User getFirstUser();
}

userDAO只是一堆方法體,它根本就沒有任何類變量,線程安全又何從談起呢?儘管多個線程都會併發訪問這個userDAO對象,但是它只是一個方法模板啊,根本沒有讀寫什麼變量,它只是負責接收不同線程傳給它的外部變量,然後返回結果。每個線程操作的都是自己的那份數據,彼此之間互不干涉,何來線程安全問題?

這個userDAO根本沒有變量,那這麼寫呢?

@Repository
public class UserDao {
private static int count=0;
public getCount(){
return count;
}
}

這個對象有一個類變量count,併發訪問的時候多個線程都會執行到getCount方法,這裡要不要考慮同步和鎖呢?答案當然是不需要,因為這裡的count只有get方法,沒有set方法或者其他改寫變量的地方,這個變量根本就是隻讀的,一個只讀不寫的變量怎麼可能存在線程安全問題呢!

對於這種沒有數據讀寫的對象我們統稱無狀態變量。

有狀態、無狀態

基本概念:

有狀態就是有數據存儲功能。有狀態對象(Stateful Bean),就是有實例變量的對象,可以保存數據,是非線程安全的。在不同方法調用間不保留任何狀態。

無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象.不能保存數據,是不變類,是線程安全的。

通過上面的分析,相信大家已經對有狀態和無狀態有了一定的理解。無狀態的Bean適合用不變模式,技術就是單例模式,這樣可以共享實例,提高性能。有狀態的Bean,多線程環境下不安全,那麼適合用Prototype原型模式,默認情況下,從Spring bean工廠所取得的實例為singleton。

Service層、Dao層用默認singleton就行,雖然Service類也有dao這樣的屬性,但dao這些類都是沒有狀態信息的,也就是相當於不變(immutable)類,所以不影響。

線程不安全的實例

那什麼情況下會發生線程不安全的事情呢,我們來看一個實例

package spx.baicai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* spring boot 入口類
*/
@RestController
public class HelloWorldController {
protected Logger log = LoggerFactory.getLogger(this.getClass());
private static int count;
@RequestMapping("/")
String home() {
log.info("index...");
count+=1;
return "count="+count;
}
}

我們的本意是要通過count這個變量實現一個計數器,每刷新一次就計數一次,實際上也確實做到了。但是它是存在安全問題的,現在我們用併發模擬200個線程請求看看

[koudai@koudai-pc bin]$ wrk -t 8 -c 200 -d 20 -T 10 http://localhost:8080/ 
Running 20s test @ http://localhost:8080/
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.90ms 5.03ms 114.65ms 92.39%
Req/Sec 3.33k 1.17k 29.24k 85.38%
530661 requests in 20.10s, 76.82MB read
Requests/sec: 26403.20
Transfer/sec: 3.82MB

報表顯示,一共執行了530661次請求,但實際上呢?

"

今天,一個小白跟我說,“我剛看了套Java面試題目,終於想清楚一個道理了,裡面說StringBuilder是線程不安全的,StringBuffer線程安全,難怪我們技術經理讓我們在寫業務代碼的時候要用StringBuffer。”

小白:都說StringBuilder線程不安全,你為啥不用StringBuffer

無語!

我一口老血噴出!這技術經理是走後門當上的吧。StringBuilder線程不安全,所以凡是用到字符串拼接的地方就改用StringBuffer?亂彈琴!知道Java裡存在線程安全,嗯,算是入門了,但是最基礎的問題要搞清楚,那就是線程安全什麼情況下會發生,什麼情況下才需要考慮,這是重點的重點。

方法內每次new的對象不存在線程安全問題

我們來看這段示例代碼:

@Service
public class UserService {
\t@Autowired
\tprotected BaseDAO<User> dao;
\t@Autowired
\tprivate UserDAO userDAO;
\tpublic boolean existUserByName(String userName,Integer sort) {
\t\tStringBuilder sbuilder = new StringBuilder("SELECT count(*) FROM {user} WHERE ";
sbuilder.append("username='");
sbuilder.append(userName);
sbuilder.append("' order by ");
sbuilder.append(sort);
\t\tint i = userDAO.queryForInt(sbuilder.toString());
\t\treturn i > 0;
\t}
}

我們先不關注拼接SQL是否規範,重點關注這裡的StringBuilder對象有沒有線程安全問題。

小白的邏輯是,這段業務代碼,在併發比較大的時候,會有線程安全問題。那事實呢?

要搞清楚的是雖然existUserByName會被多個線程同時執行到,但是做法方法裡的變量,sbuilder的作用域只存在於方法內部,並且每次都是new出來的對象。也就是說,一個Java Web的服務,每次請求進來都會對應一個線程,但是每個線程都new了自己的數據,所以這樣是不存在多線程問題的,況且userName這個參數也是外部傳入的,每一次都不一樣。

小白又說了,在Spring裡,所有的bean都默認是單例的,你說sbuilder每次都是new的,沒有線程安全問題,那麼userDAO這個對象,作為單例,所有線程共用這一個對象,有沒有安全問題?

無狀態變量不存在線程安全問題

我們來看這個userDAO對象,沒錯,它是單例,而且被共用,難道也不存在線程安全問題?

@Repository
@SqlResource("user")
public interface UserDao extends BaseMapper<User> {
int getCountWithName(@Param("name") String name);
User getFirstUser();
}

userDAO只是一堆方法體,它根本就沒有任何類變量,線程安全又何從談起呢?儘管多個線程都會併發訪問這個userDAO對象,但是它只是一個方法模板啊,根本沒有讀寫什麼變量,它只是負責接收不同線程傳給它的外部變量,然後返回結果。每個線程操作的都是自己的那份數據,彼此之間互不干涉,何來線程安全問題?

這個userDAO根本沒有變量,那這麼寫呢?

@Repository
public class UserDao {
private static int count=0;
public getCount(){
return count;
}
}

這個對象有一個類變量count,併發訪問的時候多個線程都會執行到getCount方法,這裡要不要考慮同步和鎖呢?答案當然是不需要,因為這裡的count只有get方法,沒有set方法或者其他改寫變量的地方,這個變量根本就是隻讀的,一個只讀不寫的變量怎麼可能存在線程安全問題呢!

對於這種沒有數據讀寫的對象我們統稱無狀態變量。

有狀態、無狀態

基本概念:

有狀態就是有數據存儲功能。有狀態對象(Stateful Bean),就是有實例變量的對象,可以保存數據,是非線程安全的。在不同方法調用間不保留任何狀態。

無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象.不能保存數據,是不變類,是線程安全的。

通過上面的分析,相信大家已經對有狀態和無狀態有了一定的理解。無狀態的Bean適合用不變模式,技術就是單例模式,這樣可以共享實例,提高性能。有狀態的Bean,多線程環境下不安全,那麼適合用Prototype原型模式,默認情況下,從Spring bean工廠所取得的實例為singleton。

Service層、Dao層用默認singleton就行,雖然Service類也有dao這樣的屬性,但dao這些類都是沒有狀態信息的,也就是相當於不變(immutable)類,所以不影響。

線程不安全的實例

那什麼情況下會發生線程不安全的事情呢,我們來看一個實例

package spx.baicai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* spring boot 入口類
*/
@RestController
public class HelloWorldController {
protected Logger log = LoggerFactory.getLogger(this.getClass());
private static int count;
@RequestMapping("/")
String home() {
log.info("index...");
count+=1;
return "count="+count;
}
}

我們的本意是要通過count這個變量實現一個計數器,每刷新一次就計數一次,實際上也確實做到了。但是它是存在安全問題的,現在我們用併發模擬200個線程請求看看

[koudai@koudai-pc bin]$ wrk -t 8 -c 200 -d 20 -T 10 http://localhost:8080/ 
Running 20s test @ http://localhost:8080/
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.90ms 5.03ms 114.65ms 92.39%
Req/Sec 3.33k 1.17k 29.24k 85.38%
530661 requests in 20.10s, 76.82MB read
Requests/sec: 26403.20
Transfer/sec: 3.82MB

報表顯示,一共執行了530661次請求,但實際上呢?

小白:都說StringBuilder線程不安全,你為啥不用StringBuffer

wrk

是count=530704,數據對應不上說明存在了線程安全問題。

什麼時候存在線程安全問題,什麼時候不存在,想必大家已經搞清楚了一點。說的直白點,就是在寫curd業務的時候,大可不必太多慮線程安全問題,只要注意慎用類變量基本就夠了。

"

相關推薦

推薦中...