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

