面試官:談?wù)勀銓?duì)Java線程安全與不安全的理解
當(dāng)我們查看JDK API的時(shí)候,總會(huì)發(fā)現(xiàn)一些類說(shuō)明寫著,線程安全或者線程不安全,比如說(shuō)到StringBuilder中,有這么一句,“將StringBuilder 的實(shí)例用于多個(gè)線程是不安全的。如果需要這樣的同步,則建議使用StringBuffer?!?/strong>
提到StringBuffer時(shí),說(shuō)到“StringBuffer是線程安全的可變字符序列,一個(gè)類似于String的字符串緩沖區(qū),雖然在任意時(shí)間點(diǎn)上它都包含某種特定的字符序列,但通過(guò)某些方法調(diào)用可以改變?cè)撔蛄械拈L(zhǎng)度和內(nèi)容??蓪⒆址彌_區(qū)安全地用于多個(gè)線程??梢栽诒匾獣r(shí)對(duì)這些方法進(jìn)行同步,因此任意特定實(shí)例上的所有操作就好像是以串行順序發(fā)生的,該順序與所涉及的每個(gè)線程進(jìn)行的方法調(diào)用順序一致”。
StringBuilder是一個(gè)可變的字符序列,此類提供一個(gè)與StringBuffe兼容的API,但不保證同步。該類被設(shè)計(jì)用作StringBuffer的一個(gè)簡(jiǎn)易替換,用在字符串緩沖區(qū)被單個(gè)線程使用的時(shí)候(這種情況很普遍)。如果可能,建議優(yōu)先采用該類,因?yàn)樵诖蠖鄶?shù)實(shí)現(xiàn)中,它比StringBuffer要快。將StringBuilder的實(shí)例用于多個(gè)線程是不安全的,如果需要這樣的同步,則建議使用StringBuffer。
根據(jù)以上JDK文檔中對(duì)StringBuffer和StringBuilder的描述,得到對(duì)String、StringBuilder與StringBuffer三者使用情況的總結(jié):
如果要操作少量的數(shù)據(jù)用String 單線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù)StringBuilder 多線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù)StringBuffer
那么下面手動(dòng)創(chuàng)建一個(gè)線程不安全的類,然后在多線程中使用這個(gè)類,看看有什么效果。
public class Count {
private int num;
//public void count() {
// for(int i = 1; i <= 100; i++) {
// num += i;
// }
// System.out.println(Thread.currentThread().getName() + "-" + num);
//}
public int getNum() {
return num;
}
public void increment(int i) {
num = num + i;
}
}
在這個(gè)類中的increment方法實(shí)現(xiàn)num變量與指定變量作加法。
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.increment(1);
}
System.out.println(Thread.currentThread().getName() + "-" + count.getNum());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}
這里啟動(dòng)了10個(gè)線程,看一下輸出結(jié)果:
Thread-0-1660
Thread-2-2660
Thread-3-3660
Thread-1-1660
Thread-4-4882
Thread-5-5579
Thread-6-6579
Thread-7-7579
Thread-8-8579
Thread-9-9579
期望的結(jié)果是每個(gè)線程都能輸出1000,但實(shí)際上每個(gè)線程的輸出值都不一樣而且不是整數(shù),多運(yùn)行幾次每次的輸出結(jié)果都不一樣,要想得到我們期望的結(jié)果,有幾種解決方案:
1、將累加邏輯移到Count類中,并且使用局部變量而不是成員變量;
public class Count {
public void count() {
int number = 0;
for(int i = 0; i < 1000; i++) {
number += 1;
}
System.out.println(Thread.currentThread().getName() + "-" + number);
}
}
~
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
@Override
public void run() {
count.count();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}
運(yùn)行結(jié)果如下:
Thread-0-1000
Thread-3-1000
Thread-4-1000
Thread-1-1000
Thread-2-1000
Thread-5-1000
Thread-6-1000
Thread-7-1000
Thread-8-1000
Thread-9-1000
2、將線程類成員變量拿到run方法中,這時(shí)count引用是線程內(nèi)的局部變量;
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Count count = new Count();
for (int i = 0; i < 1000; i++) {
count.increment(1);
}
System.out.println(Thread.currentThread().getName() + "-" + count.getNum());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}
運(yùn)行結(jié)果如下:
Thread-1-1000
Thread-3-1000
Thread-2-1000
Thread-0-1000
Thread-5-1000
Thread-4-1000
Thread-6-1000
Thread-7-1000
Thread-8-1000
Thread-9-1000
3、每次啟動(dòng)一個(gè)線程使用不同的線程類,不推薦。
通過(guò)上述測(cè)試,我們發(fā)現(xiàn),存在成員變量的類(即有狀態(tài)的類)用于多線程時(shí)是不安全的,不安全體現(xiàn)在這個(gè)成員變量可能發(fā)生非原子性的操作,而變量定義在方法內(nèi)也就是局部變量是線程安全的。
想想在使用struts1時(shí),不推薦創(chuàng)建成員變量,因?yàn)閍ction是單例的,如果創(chuàng)建了成員變量,就會(huì)存在線程不安全的隱患,而struts2是每一次請(qǐng)求都會(huì)創(chuàng)建一個(gè)action,就不用考慮線程安全的問題。所以,日常開發(fā)中,通常需要考慮成員變量或者說(shuō)全局變量在多線程環(huán)境下,是否會(huì)引發(fā)一些問題。
要說(shuō)明線程同步問題首先要說(shuō)明Java線程的兩個(gè)特性,可見性和有序性。
多個(gè)線程之間是不能直接傳遞數(shù)據(jù)進(jìn)行交互的,它們之間的交互只能通過(guò)共享變量來(lái)實(shí)現(xiàn)。拿上面的例子來(lái)說(shuō)明,在多個(gè)線程之間共享了Count類的一個(gè)實(shí)例,這個(gè)對(duì)象是被創(chuàng)建在主內(nèi)存(堆內(nèi)存)中,每個(gè)線程都有自己的工作內(nèi)存(線程棧),工作內(nèi)存存儲(chǔ)了主內(nèi)存count對(duì)象的一個(gè)副本,當(dāng)線程操作count對(duì)象時(shí),首先從主內(nèi)存復(fù)制count對(duì)象到工作內(nèi)存中,然后執(zhí)行代碼count.count(),改變了num值,最后用工作內(nèi)存中的count刷新主內(nèi)存的 count。當(dāng)一個(gè)對(duì)象在多個(gè)工作內(nèi)存中都存在副本時(shí),如果一個(gè)工作內(nèi)存刷新了主內(nèi)存中的共享變量,其它線程也應(yīng)該能夠看到被修改后的值,此為可見性。
多個(gè)線程執(zhí)行時(shí),CPU對(duì)線程的調(diào)度是隨機(jī)的,我們不知道當(dāng)前程序被執(zhí)行到哪步就切換到了下一個(gè)線程,一個(gè)最經(jīng)典的例子就是銀行匯款問題,一個(gè)銀行賬戶存款100,這時(shí)一個(gè)人從該賬戶取10元,同時(shí)另一個(gè)人向該賬戶匯10元,那么余額應(yīng)該還是100。那么此時(shí)可能發(fā)生這種情況,A線程負(fù)責(zé)取款,B線程負(fù)責(zé)匯款,A從主內(nèi)存讀到100,B從主內(nèi)存讀到100,A執(zhí)行減10操作,并將數(shù)據(jù)刷新到主內(nèi)存,這時(shí)主內(nèi)存數(shù)據(jù)100-10=90,而B內(nèi)存執(zhí)行加10操作,并將數(shù)據(jù)刷新到主內(nèi)存,最后主內(nèi)存數(shù)據(jù)100+10=110,顯然這是一個(gè)嚴(yán)重的問題,我們要保證A線程和B線程有序執(zhí)行,先取款后匯款或者先匯款后取款,此為有序性。
在Web開發(fā)方面,Servlet是否是線程安全的呢?
Servlet不是線程安全的。要解釋為什么Servlet為什么不是線程安全的,需要了解Servlet容器(如Tomcat)是如何響應(yīng)HTTP請(qǐng)求的。當(dāng)Tomcat接收到Client的HTTP請(qǐng)求時(shí),Tomcat從線程池中取出一個(gè)線程,之后找到該請(qǐng)求對(duì)應(yīng)的Servlet對(duì)象并進(jìn)行初始化,之后調(diào)用service()方法。
要注意的是每一個(gè)Servlet對(duì)象在Tomcat容器中只有一個(gè)實(shí)例對(duì)象,即是單例模式。如果多個(gè)HTTP請(qǐng)求請(qǐng)求的是同一個(gè)Servlet,那么這兩個(gè)HTTP請(qǐng)求對(duì)應(yīng)的線程將并發(fā)調(diào)用Servlet的service()方法。如果的Thread1和Thread2調(diào)用了同一個(gè)Servlet1,Servlet1中定義了成員變量或靜態(tài)變量,那么可能會(huì)發(fā)生線程安全問題(因?yàn)樗械木€程都可能使用這些變量)。
像Servlet這樣的類,在Web 容器中創(chuàng)建以后,會(huì)被傳遞給每個(gè)訪問Web應(yīng)用的用戶線程執(zhí)行,這個(gè)類就不是線程安全的。但這并不意味著一定會(huì)引發(fā)線程安全問題,如果Servlet類里沒有成員變量,即使多線程同時(shí)執(zhí)行這個(gè)Servlet實(shí)例的方法,也不會(huì)造成成員變量沖突。
這種對(duì)象被稱作無(wú)狀態(tài)對(duì)象,也就是說(shuō)對(duì)象不記錄狀態(tài),執(zhí)行這個(gè)對(duì)象的任何方法都不會(huì)改變對(duì)象的狀態(tài),也就不會(huì)有線程安全問題了。事實(shí)上,Web開發(fā)實(shí)踐中,常見的Service類、DAO類,都被設(shè)計(jì)成無(wú)狀態(tài)對(duì)象,所以雖然我們開發(fā)的Web應(yīng)用都是多線程的應(yīng)用,因?yàn)閃eb容器一定會(huì)創(chuàng)建多線程來(lái)執(zhí)行我們的代碼,但是我們開發(fā)中卻可以很少考慮線程安全的問題。
來(lái)源:blog.csdn.net/fuzhongmin05/article/details/59110866
我已經(jīng)更新了我的《10萬(wàn)字Springboot經(jīng)典學(xué)習(xí)筆記》中,點(diǎn)擊下面小卡片,進(jìn)入【Java開發(fā)寶典】,回復(fù):筆記,即可免費(fèi)獲取。
點(diǎn)贊是最大的支持

