什么是進(jìn)程?
電腦中時(shí)會(huì)有很多單獨(dú)運(yùn)行的程序,每個(gè)程序有一個(gè)獨(dú)立的進(jìn)程,而進(jìn)程之間是相互獨(dú)立存在的。比如下圖中的QQ、酷狗播放器、電腦管家等等。

什么是線程?
進(jìn)程想要執(zhí)行任務(wù)就需要依賴線程。換句話說(shuō),就是進(jìn)程中的最小執(zhí)行單位就是線程,并且一個(gè)進(jìn)程中至少有一個(gè)線程。
那什么是多線程?提到多線程這里要說(shuō)兩個(gè)概念,就是串行和并行,搞清楚這個(gè),我們才能更好地理解多線程。
所謂串行,其實(shí)是相對(duì)于單條線程來(lái)執(zhí)行多個(gè)任務(wù)來(lái)說(shuō)的,我們就拿下載文件來(lái)舉個(gè)例子:當(dāng)我們下載多個(gè)文件時(shí),在串行中它是按照一定的順序去進(jìn)行下載的,也就是說(shuō),必須等下載完A之后才能開(kāi)始下載B,它們?cè)跁r(shí)間上是不可能發(fā)生重疊的。

并行:下載多個(gè)文件,開(kāi)啟多條線程,多個(gè)文件同時(shí)進(jìn)行下載,這里是嚴(yán)格意義上的,在同一時(shí)刻發(fā)生的,并行在時(shí)間上是重疊的。

了解了這兩個(gè)概念之后,我們?cè)賮?lái)說(shuō)說(shuō)什么是多線程。舉個(gè)例子,我們打開(kāi)騰訊管家,騰訊管家本身就是一個(gè)程序,也就是說(shuō)它就是一個(gè)進(jìn)程,它里面有很多的功能,我們可以看下圖,能查殺病毒、清理垃圾、電腦加速等眾多功能。
按照單線程來(lái)說(shuō),無(wú)論你想要清理垃圾、還是要病毒查殺,那么你必須先做完其中的一件事,才能做下一件事,這里面是有一個(gè)執(zhí)行順序的。
如果是多線程的話,我們其實(shí)在清理垃圾的時(shí)候,還可以進(jìn)行查殺病毒、電腦加速等等其他的操作,這個(gè)是嚴(yán)格意義上的同一時(shí)刻發(fā)生的,沒(méi)有執(zhí)行上的先后順序。

以上就是,一個(gè)進(jìn)程運(yùn)行時(shí)產(chǎn)生了多個(gè)線程。
在了解完這個(gè)問(wèn)題后,我們又需要去了解一個(gè)使用多線程不得不考慮的問(wèn)題——線程安全。
今天我們不說(shuō)如何保證一個(gè)線程的安全,我們聊聊什么是線程安全?因?yàn)槲抑懊嬖嚤粏?wèn)到了,說(shuō)真的,我之前真的不是特別了解這個(gè)問(wèn)題,我們好像只學(xué)了如何確保一個(gè)線程安全,卻不知道所謂的安全到底是什么!
什么是線程安全?
當(dāng)多個(gè)線程訪問(wèn)某個(gè)方法時(shí),不管你通過(guò)怎樣的調(diào)用方式、或者說(shuō)這些線程如何交替地執(zhí)行,我們?cè)谥鞒绦蛑胁恍枰プ鋈魏蔚耐剑@個(gè)類的結(jié)果行為都是我們?cè)O(shè)想的正確行為,那么我們就可以說(shuō)這個(gè)類是線程安全的。既然是線程安全問(wèn)題,那么毫無(wú)疑問(wèn),所有的隱患都是在多個(gè)線程訪問(wèn)的情況下產(chǎn)生的,也就是我們要確保在多條線程訪問(wèn)的時(shí)候,我們的程序還能按照我們預(yù)期的行為去執(zhí)行,我們看一下下面的代碼。
Integer count = 0;
public void getCount() {
count ++;
System.out.println(count);
}
很簡(jiǎn)單的一段代碼,下面我們就來(lái)統(tǒng)計(jì)一下這個(gè)方法的訪問(wèn)次數(shù),多個(gè)線程同時(shí)訪問(wèn)會(huì)不會(huì)出現(xiàn)什么問(wèn)題,我開(kāi)啟的3條線程,每個(gè)線程循環(huán)10次,得到以下結(jié)果:

我們可以看到,這里出現(xiàn)了兩個(gè)26,出現(xiàn)這種情況顯然表明這個(gè)方法根本就不是線程安全的,出現(xiàn)這種問(wèn)題的原因有很多。
最常見(jiàn)的一種,就是我們A線程在進(jìn)入方法后,拿到了count的值,剛把這個(gè)值讀取出來(lái),還沒(méi)有改變count的值的時(shí)候,結(jié)果線程B也進(jìn)來(lái)的,那么導(dǎo)致線程A和線程B拿到的count值是一樣的。
那么由此我們可以了解到,這確實(shí)不是一個(gè)線程安全的類,因?yàn)樗麄兌夹枰僮鬟@個(gè)共享的變量。其實(shí)要對(duì)線程安全問(wèn)題給出一個(gè)明確的定義,還是蠻復(fù)雜的,我們根據(jù)我們這個(gè)程序來(lái)總結(jié)下什么是線程安全。
當(dāng)多個(gè)線程訪問(wèn)某個(gè)方法時(shí),不管你通過(guò)怎樣的調(diào)用方式、或者說(shuō)這些線程如何交替地執(zhí)行,我們?cè)谥鞒绦蛑胁恍枰プ鋈魏蔚耐剑@個(gè)類的結(jié)果行為都是我們?cè)O(shè)想的正確行為,那么我們就可以說(shuō)這個(gè)類是線程安全的。
搞清楚了什么是線程安全,接下來(lái)我們看看JAVA中確保線程安全最常用的兩種方式。先來(lái)看段代碼。
public void threadMethod(int j) {
int i = 1;
j = j + i;
}
1234567
大家覺(jué)得這段代碼是線程安全的嗎?
毫無(wú)疑問(wèn),它絕對(duì)是線程安全的,我們來(lái)分析一下,為什么它是線程安全的?
我們可以看到這段代碼是沒(méi)有任何狀態(tài)的,就是說(shuō)我們這段代碼,不包含任何的作用域,也沒(méi)有去引用其他類中的域進(jìn)行引用,它所執(zhí)行的作用范圍與執(zhí)行結(jié)果只存在它這條線程的局部變量中,并且只能由正在執(zhí)行的線程進(jìn)行訪問(wèn)。當(dāng)前線程的訪問(wèn),不會(huì)對(duì)另一個(gè)訪問(wèn)同一個(gè)方法的線程造成任何的影響。
兩個(gè)線程同時(shí)訪問(wèn)這個(gè)方法,因?yàn)闆](méi)有共享的數(shù)據(jù),所以他們之間的行為,并不會(huì)影響其他線程的操作和結(jié)果,所以說(shuō)無(wú)狀態(tài)的對(duì)象,也是線程安全的。
添加一個(gè)狀態(tài)呢?
如果我們給這段代碼添加一個(gè)狀態(tài),添加一個(gè)count,來(lái)記錄這個(gè)方法并命中的次數(shù),每請(qǐng)求一次count+1,那么這個(gè)時(shí)候這個(gè)線程還是安全的嗎?
public class ThreadDemo {
int count = 0; // 記錄方法的命中次數(shù)
public void threadMethod(int j) {
count++ ;
int i = 1;
j = j + i;
}
}
1234567891011121314
明顯已經(jīng)不是了,單線程運(yùn)行起來(lái)確實(shí)是沒(méi)有任何問(wèn)題的,但是當(dāng)出現(xiàn)多條線程并發(fā)訪問(wèn)這個(gè)方法的時(shí)候,問(wèn)題就出現(xiàn)了,我們先來(lái)分析下count+1這個(gè)操作。
進(jìn)入這個(gè)方法之后首先要讀取count的值,然后修改count的值,最后才把這把值賦值給count,總共包含了三步過(guò)程:“讀取”一>“修改”一>“賦值”,既然這個(gè)過(guò)程是分步的,那么我們先來(lái)看下面這張圖,看看你能不能看出問(wèn)題:

可以發(fā)現(xiàn),count的值并不是正確的結(jié)果,當(dāng)線程A讀取到count的值,但是還沒(méi)有進(jìn)行修改的時(shí)候,線程B已經(jīng)進(jìn)來(lái)了,然后線程B讀取到的還是count為1的值,正因?yàn)槿绱怂晕覀兊腸ount值已經(jīng)出現(xiàn)了偏差,那么這樣的程序放在我們的代碼中,是存在很多的隱患的。
如何確保線程安全?
既然存在線程安全的問(wèn)題,那么肯定得想辦法解決這個(gè)問(wèn)題,怎么解決?我們說(shuō)說(shuō)常見(jiàn)的幾種方式
synchronized
synchronized關(guān)鍵字,就是用來(lái)控制線程同步的,保證我們的線程在多線程環(huán)境下,不被多個(gè)線程同時(shí)執(zhí)行,確保我們數(shù)據(jù)的完整性,使用方法一般是加在方法上。
public class ThreadDemo {
int count = 0; // 記錄方法的命中次數(shù)
public synchronized void threadMethod(int j) {
count++ ;
int i = 1;
j = j + i;
}
}
1234567891011121314
這樣就可以確保我們的線程同步了,同時(shí)這里需要注意一個(gè)大家平時(shí)忽略的問(wèn)題,首先synchronized鎖的是括號(hào)里的對(duì)象,而不是代碼,其次,對(duì)于非靜態(tài)的synchronized方法,鎖的是對(duì)象本身也就是this。
當(dāng)synchronized鎖住一個(gè)對(duì)象之后,別的線程如果想要獲取鎖對(duì)象,那么就必須等這個(gè)線程執(zhí)行完釋放鎖對(duì)象之后才可以,否則一直處于等待狀態(tài)。
注意點(diǎn):雖然加synchronized關(guān)鍵字,可以讓我們的線程變得安全,但是我們?cè)谟玫臅r(shí)候,也要注意縮小synchronized的使用范圍,如果隨意使用時(shí)很影響程序的性能,別的對(duì)象想拿到鎖,結(jié)果你沒(méi)用鎖還一直把鎖占用,這樣就有點(diǎn)浪費(fèi)資源。
lock
先來(lái)說(shuō)說(shuō)它跟synchronized有什么區(qū)別吧,Lock是在Java1.6被引入進(jìn)來(lái)的,Lock的引入讓鎖有了可操作性,什么意思?就是我們?cè)谛枰臅r(shí)候去手動(dòng)的獲取鎖和釋放鎖,甚至我們還可以中斷獲取以及超時(shí)獲取的同步特性,但是從使用上說(shuō)Lock明顯沒(méi)有synchronized使用起來(lái)方便快捷。我們先來(lái)看下一般是如何使用的:
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類
private void method(Thread thread){
lock.lock(); // 獲取鎖對(duì)象
try {
System.out.println("線程名:"+thread.getName() + "獲得了鎖");
// Thread.sleep(2000);
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("線程名:"+thread.getName() + "釋放了鎖");
lock.unlock(); // 釋放鎖對(duì)象
}
}
123456789101112131415
進(jìn)入方法我們首先要獲取到鎖,然后去執(zhí)行我們業(yè)務(wù)代碼,這里跟synchronized不同的是,Lock獲取的所對(duì)象需要我們親自去進(jìn)行釋放,為了防止我們代碼出現(xiàn)異常,所以我們的釋放鎖操作放在finally中,因?yàn)閒inally中的代碼無(wú)論如何都是會(huì)執(zhí)行的。
寫(xiě)個(gè)主方法,開(kāi)啟兩個(gè)線程測(cè)試一下我們的程序是否正常:
public static void main(String[] args) {
LockTest lockTest = new LockTest();
// 線程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// Thread.currentThread() 返回當(dāng)前線程的引用
lockTest.method(Thread.currentThread());
}
}, "t1");
// 線程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t2");
t1.start();
t2.start();
}
1234567891011121314151617181920212223242526
結(jié)果

可以看出我們的執(zhí)行,是沒(méi)有任何問(wèn)題的。
其實(shí)在Lock還有幾種獲取鎖的方式,我們這里再說(shuō)一種,就是tryLock()這個(gè)方法跟Lock()是有區(qū)別的,Lock在獲取鎖的時(shí)候,如果拿不到鎖,就一直處于等待狀態(tài),直到拿到鎖,但是tryLock()卻不是這樣的,tryLock是有一個(gè)Boolean的返回值的,如果沒(méi)有拿到鎖,直接返回false,停止等待,它不會(huì)像Lock()那樣去一直等待獲取鎖。
我們來(lái)看下代碼:
private void method(Thread thread){
// lock.lock(); // 獲取鎖對(duì)象
if (lock.tryLock()) {
try {
System.out.println("線程名:"+thread.getName() + "獲得了鎖");
// Thread.sleep(2000);
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("線程名:"+thread.getName() + "釋放了鎖");
lock.unlock(); // 釋放鎖對(duì)象
}
}
}
123456789101112131415
結(jié)果:我們繼續(xù)使用剛才的兩個(gè)線程進(jìn)行測(cè)試可以發(fā)現(xiàn),在線程t1獲取到鎖之后,線程t2立馬進(jìn)來(lái),然后發(fā)現(xiàn)鎖已經(jīng)被占用,那么這個(gè)時(shí)候它也不在繼續(xù)等待。
似乎這種方法,感覺(jué)不是很完美,如果我第一個(gè)線程,拿到鎖的時(shí)間,比第二個(gè)線程進(jìn)來(lái)的時(shí)間還要長(zhǎng),是不是也拿不到鎖對(duì)象?
那我能不能,用一中方式來(lái)控制一下,讓后面等待的線程,可以等待5秒,如果5秒之后,還獲取不到鎖,那么就停止等,其實(shí)tryLock()是可以進(jìn)行設(shè)置等待的相應(yīng)時(shí)間的。
private%20void%20method(Thread%20thread)%20throws%20InterruptedException%20{
%20%20%20%20%20%20%20//%20lock.lock();%20//%20獲取鎖對(duì)象
%20%20%20%20%20%20%20//%20如果2秒內(nèi)獲取不到鎖對(duì)象,那就不再等待
%20%20%20%20%20%20%20if%20(lock.tryLock(2,TimeUnit.SECONDS))%20{
%20%20%20%20%20%20%20%20%20%20%20try%20{
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20System.out.println("線程名:"+thread.getName()%20+%20"獲得了鎖");
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20這里睡眠3秒
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Thread.sleep(3000);
%20%20%20%20%20%20%20%20%20%20%20}catch(Exception%20e){
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20e.printStackTrace();
%20%20%20%20%20%20%20%20%20%20%20}%20finally%20{
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20System.out.println("線程名:"+thread.getName()%20+%20"釋放了鎖");
%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20lock.unlock();%20//%20釋放鎖對(duì)象
%20%20%20%20%20%20%20%20%20%20%20}
%20%20%20%20%20%20%20}
%20%20%20}
12345678910111213141516171819
結(jié)果:看上面的代碼,我們可以發(fā)現(xiàn),雖然我們獲取鎖對(duì)象的時(shí)候,可以等待2秒,但是我們線程t1在獲取鎖對(duì)象之后,執(zhí)行任務(wù)缺花費(fèi)了3秒,那么這個(gè)時(shí)候線程t2是不在等待的。
我們?cè)賮?lái)改一下這個(gè)等待時(shí)間,改為5秒,再來(lái)看下結(jié)果:
private void method(Thread thread) throws InterruptedException {
// lock.lock(); // 獲取鎖對(duì)象
// 如果5秒內(nèi)獲取不到鎖對(duì)象,那就不再等待
if (lock.tryLock(5,TimeUnit.SECONDS)) {
try {
System.out.println("線程名:"+thread.getName() + "獲得了鎖");
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("線程名:"+thread.getName() + "釋放了鎖");
lock.unlock(); // 釋放鎖對(duì)象
}
}
}
12345678910111213141516
結(jié)果:這個(gè)時(shí)候我們可以看到,線程t2等到5秒獲取到了鎖對(duì)象,執(zhí)行了任務(wù)代碼。
