一、進(jìn)程和線程
進(jìn)程
進(jìn)程就是一個(gè)執(zhí)行中的程序?qū)嵗?/strong>,每個(gè)進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間,一個(gè)進(jìn)程中可以有多個(gè)線程。比如在Windows系統(tǒng)中,一個(gè)運(yùn)行的xx.exe就是一個(gè)進(jìn)程。
- 每個(gè)進(jìn)程有各自獨(dú)立的一塊內(nèi)存,使得各個(gè)進(jìn)程之間內(nèi)存地址相互隔離。
線程
線程是指進(jìn)程中的一個(gè)執(zhí)行任務(wù)(控制單元),一個(gè)進(jìn)程中可以運(yùn)行多個(gè)線程。
同一個(gè)進(jìn)程間的多個(gè)線程共享該進(jìn)程的數(shù)據(jù),
通常情況下:
1、多線程在數(shù)據(jù)共享上要比多進(jìn)程更加便捷。
2、一個(gè)進(jìn)程使用多線程,通過提高cpu使用率可以提高效率,因?yàn)槎嗑€程可以有效的使用系統(tǒng)的資源和提高系統(tǒng)的吞吐量(單位時(shí)間內(nèi)執(zhí)行的指令數(shù))
線程本身本身并不能提高效率,是曲線救國通過提高資源使用效率來提高系統(tǒng)的效率
3、 Java程序的進(jìn)程里至少有這么幾個(gè)線程:主線程, 垃圾回收線程(后臺(tái)線程)
4、因?yàn)镃PU在瞬間不斷切換去處理各個(gè)線程,導(dǎo)致了多線程的執(zhí)行具有隨機(jī)性
.
.
一個(gè)標(biāo)準(zhǔn)的線程,由線程ID、當(dāng)前指令指針(PC)、寄存器和堆棧組成。
一個(gè)標(biāo)準(zhǔn)的進(jìn)程,由內(nèi)存空間(代碼、數(shù)據(jù)、進(jìn)程空間、打開的文件)和一個(gè)或多個(gè)線程組成。
—— [ 編程思想之多線程與多進(jìn)程(1)——以操作系統(tǒng)的角度述說線程與進(jìn)程 ]


二、并發(fā)和并行
- 單核cpu,并發(fā)
- 多核cpu,并行
單核并發(fā)
在單核機(jī)器上,“多進(jìn)程”并不是真正的多個(gè)進(jìn)程在同時(shí)執(zhí)行,而是通過CPU時(shí)間分片,操作系統(tǒng)快速在進(jìn)程間切換而模擬出來的多進(jìn)程。我們通常把這種情況成為并發(fā)。
單核的多個(gè)進(jìn)程,不是“一并發(fā)生”的,是cpu高速切換讓我們看起來像“一并發(fā)生”而已。
多核并行
我們使用的計(jì)算機(jī)基本上都搭載了多核CPU,這時(shí),我們能真正的實(shí)現(xiàn)多個(gè)進(jìn)程并行執(zhí)行,這種情況叫做并行(一并進(jìn)行)。
多核cpu讓多進(jìn)程的并發(fā)有可能變成了并行。
所以我們說得并發(fā),有可能是是并行,也可能是并發(fā),這跟cpu的核心數(shù)有關(guān)系。
在多核機(jī)器上,我們的多個(gè)線程可以并行執(zhí)行在多個(gè)核上,進(jìn)一步提升效率。
例子
舉一個(gè)并發(fā)的小例子,多線程下載文件。
比如我們有一個(gè)9m的文件要下載,使用3個(gè)線程來下載,那么每個(gè)線程下載的分到現(xiàn)在大小為3m。
一方面,因?yàn)榫€程搶占cpu具有隨機(jī)性,多線程更加容易搶占到被cpu執(zhí)行機(jī)會(huì)。
另外一方面,并行/并行工作,會(huì)單線程快一些。
三、線程的創(chuàng)建方式
Java的線程有2種創(chuàng)建方式
第一種,繼承Thread類
直接定義一個(gè)Thread的子類并實(shí)例化,從而創(chuàng)建一個(gè)新線程。通過start創(chuàng)建線程
class MyThread extends Thread {
public void run() {
//這里是線程要執(zhí)行的任務(wù)
}
}
直接對(duì)其調(diào)用start方法,即可啟動(dòng)這個(gè)線程:
t.start();
第二種方式,實(shí)現(xiàn) Runnable 接口,
實(shí)現(xiàn) Runnable 接口,把 Runnable 作為參數(shù)傳入 Thread 的構(gòu)造函數(shù),通過start創(chuàng)建線程
class MyRunnable implements Runnable {
...
public void run() {
//這里是新線程需要執(zhí)行的任務(wù)
}
}
Runnable r = new MyRunnable();
Thread t = new Thread(r);
調(diào)用start方法,即可啟動(dòng)這個(gè)線程:
t.start();
.
.
來一個(gè)例子吧
線程的啟動(dòng)
public class TestClass {
public static void main(String[] args) {
System.out.println("main 當(dāng)前線程:"+Thread.currentThread().getName());
System.out.println("========");
new MyThread().start();
new Thread(new RunThread()).start();
}
}
class MyThread extends Thread{
public void run(){
//super.run();
System.out.println("MyThread run方法執(zhí)行");
System.out.println("MyThread run 當(dāng)前線程 "+Thread.currentThread().getName());
System.out.println("========");
}
}
class RunThread implements Runnable{
@Override
public void run() {
System.out.println("RunThread run方法執(zhí)行");
System.out.println("RunThread run 當(dāng)前線程 "+Thread.currentThread().getName());
System.out.println("========");
}
}
.
打印
main 當(dāng)前線程:main
========
MyThread run方法執(zhí)行
MyThread run 當(dāng)前線程 Thread-0
========
RunThread run方法執(zhí)行
RunThread run 當(dāng)前線程 Thread-1
========
注:+Thread.currentThread().getName()+Thread.currentThread().getName() 可以獲得當(dāng)前線程的名稱
.
.
從上面的代碼中,雖然看起來很有規(guī)律,我們知道線程不是一旦start啟動(dòng)就會(huì)被執(zhí)行,很可能有個(gè)等待的過程,被cpu隨機(jī)切換才正式工作,工作有可能隨時(shí)被打斷,后面我們會(huì)再看,現(xiàn)在先來一份簡單示例
.
.
線程的被cpu調(diào)度具有隨機(jī)性
public class TestClass {
public static void main(String[] args) {
for(int y = 0;y<6;y++){
new Thread(new RunThread()).start();
System.out.println("啟動(dòng) 第"+ y +"個(gè)線程");
}
}
}
class RunThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println("run work"+i);
}
}
}
.
.
打印
啟動(dòng) 第0個(gè)線程
run work0
run work1
run work2
啟動(dòng) 第1個(gè)線程
run work0
run work1
啟動(dòng) 第2個(gè)線程
run work2
run work0
啟動(dòng) 第3個(gè)線程
run work1
run work2
run work0
啟動(dòng) 第4個(gè)線程
run work1
run work0
run work2
啟動(dòng) 第5個(gè)線程
run work0
run work1
run work1
run work2
run work2
1、打印結(jié)果不唯一,隨機(jī)
2、繼承自Thread和實(shí)現(xiàn)Runnable都一樣,就不另附代碼了
.
.
為線程指定名稱
主線程默認(rèn)名稱為 main
其他線程默認(rèn)的名稱是 Thread-數(shù)字,但是我們可以通過給寫一個(gè)構(gòu)造函數(shù),在構(gòu)造函數(shù)里面出入字符串給super的方式給線程指定名稱。
我們?cè)賮砜匆环荽a。
public class TestClass {
public static void main(String[] args) {
new TestThreadA().start();
new TestThreadB("張三").start();
}
}
class TestThreadA extends Thread{
@Override
public void run() {
System.out.println("TestThreadA getName "+ getName());
super.run();
}
}
class TestThreadB extends Thread{
TestThreadB(String str){
super(str);
}
@Override
public void run() {
System.out.println("TestThreadB getName "+ getName());
super.run();
}
}
.
.
輸出
TestThreadA getName Thread-0
TestThreadB getName 張三
.
.
兩種創(chuàng)建方式對(duì)比
**對(duì)于第一種方式,繼承Thread類 **
A extends Thread:
- 同份資源不共享
- 無法繼承其他類了
**對(duì)于第二種方式,實(shí)現(xiàn) Runnable 接口作為Thread類構(gòu)造函數(shù) **
class MyRunnable implements Runnable
- 多個(gè)線程共享一個(gè)目標(biāo)資源,適合多線程處理同一份資源。
- 該類還可以繼承其他類
第二種相對(duì)比較推薦。
關(guān)于run和statr方法
start()
start()方法的作用是啟動(dòng)一個(gè)新線程,新線程會(huì)執(zhí)行相應(yīng)的run()方法。start()不能被重復(fù)調(diào)用。
run()
run()就和普通的成員方法一樣,可以被重復(fù)調(diào)用。單獨(dú)調(diào)用run()的話,會(huì)在當(dāng)前線程中執(zhí)行run(),而并不會(huì)啟動(dòng)新線程!
三、線程的生命周期/狀態(tài)
Thread類內(nèi)部有個(gè)public的枚舉Thread.State,里邊將線程的狀態(tài)分為:
New(新生)
NEW(新建尚未運(yùn)行/啟動(dòng))
Runnable(可運(yùn)行)
處于可運(yùn)行狀態(tài):正在運(yùn)行或準(zhǔn)備運(yùn)行
在線程對(duì)象上調(diào)用start方法后,相應(yīng)線程便會(huì)進(jìn)入Runnable狀態(tài),若被線程調(diào)度程序調(diào)度,這個(gè)線程便會(huì)成為當(dāng)前運(yùn)行(Running)的線程;
Blocked(被阻塞)
阻塞狀態(tài),受阻塞并等待某個(gè)監(jiān)視器鎖的線程,處于這種狀態(tài)。
若一段代碼被線程A “上鎖” ,此時(shí)線程B嘗試執(zhí)行這段代碼,線程B就會(huì)進(jìn)入Blocked狀態(tài);
Waiting(等待)
通過wait方法進(jìn)入的等待
當(dāng)線程等待另一個(gè)線程通知線程調(diào)度器一個(gè)條件時(shí),它本身就會(huì)進(jìn)入Waiting狀態(tài);
Time Waiting(計(jì)時(shí)等待)
通過sleep或wait timeout方法進(jìn)入的限期等待的狀態(tài)
計(jì)時(shí)等待與等待的區(qū)別是,線程只等待一定的時(shí)間,若超時(shí)則不再等待;
Terminated(被終止)
線程終止?fàn)顟B(tài)
線程的run方法執(zhí)行完畢或者由于一個(gè)未捕獲的異常導(dǎo)致run方法意外終止會(huì)進(jìn)入Terminated狀態(tài)。
口頭的“阻塞”統(tǒng)一指代Blocked、Waiting、Time Waiting其中任一。
來個(gè)簡圖

四、線程的基本方法/控制線程
要了解的有如下方法
- start()
- Thread.sleep()
- interrupt
- isAlive()
- join()
- yield()
- wait()
- **notify() 和 notifyAll() **
- getPriority() 和 setPriority()
(下文涉及到多線程的代碼運(yùn)行輸出部分,結(jié)果并不是一定是唯一的,因?yàn)閏pu調(diào)度線程是隨機(jī)的)
四.1 start()
這是一個(gè)實(shí)例方法,啟動(dòng)一個(gè)線程,但是線程不是已啟動(dòng)就會(huì)執(zhí)行
四.2 isAlive()
這是一個(gè)實(shí)例方法, 用于判斷當(dāng)前線程是否還“活著”,即線程是否終止,返回true即為“活著”
示例
public class TestClass {
public static void main(String[] args) {
MyRunnable runTh = new MyRunnable();
Thread t1 = new Thread(runTh);
System.out.println("isAlive "+ t1.isAlive());
t1.start();
System.out.println("isAlive "+ t1.isAlive());
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("run work");
}
}
.
.
打印
isAlive false
isAlive true
run work
.
.
四.3 interrupt()
interrupt本身是 中斷,打斷 的意思
這是一個(gè)實(shí)例方法。每個(gè)線程都有一個(gè) 中斷狀態(tài) 標(biāo)識(shí),如果調(diào)用 interrupt 會(huì)將相應(yīng)線程的 中斷狀態(tài) 標(biāo)記為 true,再通過 isInterrupted() 方法即可獲得 中斷標(biāo)志 為true。
調(diào)用interrupt,能夠 打斷 那些通過調(diào)用 可中斷方法 進(jìn)入 阻塞狀態(tài) 的線程。
常見的可中斷方法有sleep、wait、join,這些方法的內(nèi)部實(shí)現(xiàn)會(huì)時(shí)不時(shí)的檢查當(dāng)前線程的中斷狀態(tài),若為true會(huì)立刻拋出一個(gè)InterruptedException異常,從而中斷當(dāng)前線程。
中斷捕獲異常后,是決定讓線程繼續(xù)運(yùn)行,還是結(jié)束等要根據(jù)業(yè)務(wù)場景才處理。
注:注意,如果線程從來沒有調(diào)用 可中斷方法 ,然后我們就去調(diào)用 interrupt 那么線程是不會(huì)被中斷的。interrupt能夠讓線程中斷的核心原因是sleep、wait、join等可中斷方法的內(nèi)部檢查到interrupt狀態(tài)位true拋異常造成的。
使用 interrupt 來停止線程是比較粗暴的(interrupt并不能讓線程終止),還有另外一個(gè)方法,stop()方法(stop會(huì)直接線程終止),這個(gè)方法已經(jīng)被棄用,這個(gè)方法更加粗暴。應(yīng)該怎么合理停止線程我們后面會(huì)涉及到。
待會(huì)我們會(huì)結(jié)合sleep方法和interrupt方法寫一份小demo
四.4 Thread.sleep()
給當(dāng)前線程指定睡眠毫秒值
結(jié)合sleep和interrupt方法的簡單示例
import java.util.Date;
public class TestClass {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
// 這里我們讓主線程睡眠3秒,睡眠期間下面的myThread.interrupt();不會(huì)被執(zhí)行
// 主線程睡眠期間子線程myThread愉快地每隔一秒打印一次時(shí)間
Thread.sleep(3000);
}catch (InterruptedException e) {
}
// 主線程3秒過后,接著工作,myThread.interrupt()被執(zhí)行,myThread被終端,不再打印時(shí)間
// 我們這里myThread.interrupt();可以順利中斷myThread是因?yàn)閙yThread之前調(diào)用了可中斷方法sleep
myThread.interrupt();
System.out.println("=== myThread.interrupt() 被執(zhí)行 ===");
}
}
class MyThread extends Thread {
boolean flag = true;
public void run() {
// 正常來說,不被影響的下面這段代碼會(huì)1秒打印一次當(dāng)前時(shí)間
while (flag) {
System.out.println("===" + new Date() + "===");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("===捕獲 InterruptedException ===");
return;
}
}
}
}
.
輸出
===Sun Apr 30 17:09:28 ICT 2017===
===Sun Apr 30 17:09:29 ICT 2017===
===Sun Apr 30 17:09:30 ICT 2017===
=== myThread.interrupt() 被執(zhí)行 ===
===捕獲 InterruptedException ===
可見,myThread.interrupt()成功地中斷了myThread線程。
.
.
四.5 join()
這是一個(gè)實(shí)例方法,在A線程中對(duì)線程B調(diào)用join方法會(huì)導(dǎo)致A線程暫時(shí)處于waiting,等線程B運(yùn)行完畢后再接著運(yùn)行線程A。
也就是說,把當(dāng)前線程還沒執(zhí)行的部分“接到”另一個(gè)線程后面去,另一個(gè)線程運(yùn)行完畢后,當(dāng)前線程再接著運(yùn)行。join方法有以下重載版本:
public final synchronized void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos) throws InterruptedException;
無參數(shù)的join表示當(dāng)前線程一直等到另一個(gè)線程運(yùn)行完畢,這種情況下當(dāng)前線程會(huì)處于Wating狀態(tài);
帶參數(shù)的表示當(dāng)前線程只等待指定的時(shí)間,這種情況下當(dāng)前線程會(huì)處于Time Waiting狀態(tài)。當(dāng)前線程通過調(diào)用join方法進(jìn)入Time Waiting或Waiting狀態(tài)后,會(huì)釋放已經(jīng)獲取的鎖。實(shí)際上,join方法內(nèi)部調(diào)用了Object類的實(shí)例方法wait
join改變線程執(zhí)行結(jié)果
示例代碼
import java.util.Date;
public class TestClass {
public static void main(String[] args) {
System.out.println("主線程工作中 "+Thread.currentThread().getName());
MyThread myThread = new MyThread();
myThread.start();
System.out.println("主線程 的狀態(tài)(子線程join之前) "+Thread.currentThread().getState());
// 子線程 join,主線程停止等待
try {
myThread.join();
System.out.println("主線程 的狀態(tài)(子線程join之后) "+Thread.currentThread().getState());
}catch (InterruptedException e) {
System.out.println("===主線程 捕獲 InterruptedException ===");
}
for(int i=0;i<8;i++){
System.out.println("主線程for循環(huán) "+Thread.currentThread().getName() +" == "+i);
}
}
}
class MyThread extends Thread {
public void run() {
for(int i=0;i<5;i++){
System.out.println("===" + new Date() + "===");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("===捕獲 InterruptedException ===");
return;
}
}
}
}
輸出
主線程工作中 main
主線程 的狀態(tài)(子線程join之前) RUNNABLE
===Sun Apr 30 20:00:07 ICT 2017===
===Sun Apr 30 20:00:08 ICT 2017===
===Sun Apr 30 20:00:09 ICT 2017===
===Sun Apr 30 20:00:10 ICT 2017===
===Sun Apr 30 20:00:11 ICT 2017===
主線程 的狀態(tài)(子線程join之后) RUNNABLE
主線程for循環(huán) main == 0
主線程for循環(huán) main == 1
主線程for循環(huán) main == 2
主線程for循環(huán) main == 3
主線程for循環(huán) main == 4
主線程for循環(huán) main == 5
主線程for循環(huán) main == 6
主線程for循環(huán) main == 7
可以看到,在myThread.start();執(zhí)行之后,主線程停止執(zhí)行,直到子線程的run工作后主線程再繼續(xù)工作。
如果我們上面代碼中的
myThread.join();
這行代碼和其try備注掉,那么打印結(jié)果很可能就是
主線程工作中 main
主線程 的狀態(tài)(子線程join之前) RUNNABLE
主線程for循環(huán) main == 0
主線程for循環(huán) main == 1
主線程for循環(huán) main == 2
主線程for循環(huán) main == 3
主線程for循環(huán) main == 4
主線程for循環(huán) main == 5
主線程for循環(huán) main == 6
主線程for循環(huán) main == 7
===Sun Apr 30 20:04:03 ICT 2017===
===Sun Apr 30 20:04:05 ICT 2017===
===Sun Apr 30 20:04:06 ICT 2017===
===Sun Apr 30 20:04:07 ICT 2017===
===Sun Apr 30 20:04:08 ICT 2017===
通過這兩段輸出,我們可以看到j(luò)oin的作用了。
還有,子線程調(diào)用join時(shí),主線程還是Runnable狀態(tài)
四.6 getPriority() 和 setPriority()
getPriority() 獲得線程的優(yōu)先級(jí)數(shù)值
setPriority() 設(shè)置線程的優(yōu)先級(jí)數(shù)值
優(yōu)先級(jí)范圍
線程存在優(yōu)先級(jí),優(yōu)先級(jí)范圍在1~10之間。
默認(rèn)優(yōu)先級(jí)和優(yōu)先級(jí)常量
線程默認(rèn)優(yōu)先級(jí)是5,Thread類中有三個(gè)常量,定義線程優(yōu)先級(jí)范圍:
static int MAX_PRIORITY
線程可以具有的最高優(yōu)先級(jí)。
static int MIN_PRIORITY
線程可以具有的最低優(yōu)先級(jí)。
static int NORM_PRIORITY
分配給線程的默認(rèn)優(yōu)先級(jí)。
JVM線程調(diào)度程序是基于優(yōu)先級(jí)的調(diào)度機(jī)制。在大多數(shù)情況下,
1、當(dāng)前運(yùn)行的線程優(yōu)先級(jí)將大于或等于線程池中任何線程的優(yōu)先級(jí)。
2、優(yōu)先級(jí)高的線程被cpu調(diào)度的概率大于優(yōu)先級(jí)低的線程
1、不是說優(yōu)先級(jí)低在在優(yōu)先級(jí)高的面前就不執(zhí)行了,只是優(yōu)先級(jí)高的執(zhí)行頻率比較高,而優(yōu)先級(jí)低的執(zhí)行一小會(huì)就會(huì)被趕出來。
2、當(dāng)設(shè)計(jì)多線程應(yīng)用程序的時(shí)候,一定不要依賴于線程的優(yōu)先級(jí)。因?yàn)榫€程調(diào)度優(yōu)先級(jí)操作是沒有保障的,只能把線程優(yōu)先級(jí)作用作為一種提高程序效率的方法,但是要保證程序不依賴這種操作。
設(shè)置優(yōu)先級(jí)
示例代碼
public class TestPriority {
public static void main(String[] args) {
Thread t1 = new Thread(new T1());
Thread t2 = new Thread(new T2());
t1.setPriority(Thread.NORM_PRIORITY+3); //設(shè)置優(yōu)先級(jí)的值
t1.start();
t2.start();
}
}
class T1 implements Runnable {
public void run() {
for(int i = 0 ;i <50 ; i++)
System.out.println("T1:"+i);
}
}
class T2 implements Runnable {
public void run() {
for(int i = 0 ;i <50 ; i++)
System.out.println("------T2::"+i);
}
}
四.7 yield()
這是一個(gè)靜態(tài)方法,作用是讓當(dāng)前線程“讓步”,目的是讓其他線程有更大的可能被系統(tǒng)調(diào)度,這個(gè)方法不會(huì)釋放鎖。
yield() 的“讓步”只是讓一小會(huì),一小會(huì)之后就接著工作了。
yield操作時(shí),線程還是Runnable狀態(tài)。
調(diào)用yield()做出讓步
代碼
public class TestClass {
public static void main(String[] args) {
MyThreadA myThreadA = new MyThreadA();
MyThreadB myThreadB = new MyThreadB();
myThreadA.start();
myThreadB.start();
}
}
class MyThreadA extends Thread {
public void run() {
for(int i=0;i<12;i++){
System.out.println("MyThreadA run work "+i);
}
}
}
class MyThreadB extends Thread {
public void run() {
for(int i=0;i<12;i++){
if(i/3 == 0){
// 當(dāng)循環(huán)到模以3為0的時(shí)候,就做出一小會(huì)的“讓步”
yield();
}
System.out.println("MyThreadB ===== run work "+i);
}
}
}
.
輸出
(某一次的輸出)
MyThreadA run work 0
MyThreadB ===== run work 0
MyThreadA run work 1
MyThreadB ===== run work 1
MyThreadA run work 2
MyThreadA run work 3
MyThreadA run work 4
MyThreadA run work 5
MyThreadA run work 6
MyThreadA run work 7
MyThreadA run work 8
MyThreadB ===== run work 2
MyThreadB ===== run work 3
MyThreadB ===== run work 4
MyThreadA run work 9
MyThreadB ===== run work 5
MyThreadA run work 10
MyThreadB ===== run work 6
MyThreadA run work 11
MyThreadB ===== run work 7
MyThreadB ===== run work 8
MyThreadB ===== run work 9
MyThreadB ===== run work 10
MyThreadB ===== run work 11
四.8 wait()
wait方法是Object類中定義的實(shí)例方法。在指定對(duì)象上調(diào)用wait方法能夠讓當(dāng)前線程進(jìn)入阻塞狀態(tài)(前提時(shí)當(dāng)前線程持有該對(duì)象的內(nèi)部鎖(monitor)),此時(shí)當(dāng)前線程會(huì)釋放已經(jīng)獲取的那個(gè)對(duì)象的內(nèi)部鎖,這樣一來其他線程就可以獲取這個(gè)對(duì)象的內(nèi)部鎖了。當(dāng)其他線程獲取了這個(gè)對(duì)象的內(nèi)部鎖,進(jìn)行了一些操作后可以調(diào)用notify方法來喚醒正在等待該對(duì)象的線程。
關(guān)于wait()涉及到線程鎖和線程通信問題,后文的關(guān)于會(huì)有相關(guān)參考代碼。
.
.
四.9 **notify() 和 notifyAll() **
notify/notifyAll方法也是Object類中定義的實(shí)例方法。作用是喚醒正在等待相應(yīng)對(duì)象的線程
notify() 喚醒 wait pool 一個(gè)等待該對(duì)象的線程
notifyAll() 喚醒 wait pool 所有等待該對(duì)象的線程
關(guān)于notify()/notifyAll()涉及到線程鎖和線程通信問題,后文的關(guān)于會(huì)有相關(guān)參考代碼。
.
.
四.10 如何停止線程
注意:
1、interrupt 并無法真正停止線程
2、Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 這些終止線程運(yùn)行的方法已經(jīng)被廢棄,使用它們是極端不安全的!
正常情況下線程什么時(shí)候回停止?
run方法執(zhí)行完畢,該線程就會(huì)正常結(jié)束。
(但有時(shí)候線程是永遠(yuǎn)無法結(jié)束的,比如while(true)。)
1、利用run里面的標(biāo)志位結(jié)束線程
run里面while,用boolean標(biāo)志位控制
代碼
public class TestClass {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
// 延緩一下,不至于線程馬上關(guān)閉,為了方便打印演示結(jié)果
for(int i=0;i<30;i++){
if(i%10==0)
System.out.println("in thread main i=" + i);
}
myThread.shutDown();
System.out.println("myThread.shutDown() 執(zhí)行");
}
}
class MyThread extends Thread {
// 停止線程的標(biāo)志位 (run結(jié)束,線程就結(jié)束)
private boolean flag = true;
public void run() {
int i = 0;
while (flag == true) {
System.out.println("MyThread run " + i++);
}
}
public void shutDown() {
// 自己定義這個(gè)方法,調(diào)用時(shí)讓線程停止
flag = false;
}
}
輸出
某次輸出
in thread main i=0
in thread main i=10
MyThread run 0
MyThread run 1
in thread main i=20
myThread.shutDown() 執(zhí)行
MyThread run 2
.
.
再來一次輸出
in thread main i=0
in thread main i=10
in thread main i=20
myThread.shutDown() 執(zhí)行
MyThread run 0
可見,停止進(jìn)程后不是說run里面的代碼就絕對(duì)馬上停止,可能還會(huì)執(zhí)行個(gè)一次兩次,但是關(guān)閉確實(shí)是實(shí)現(xiàn)的了。
(有時(shí)間再補(bǔ)上其他的方式)
待添加
下面這副圖描述了線程從創(chuàng)建到消亡之間的狀態(tài):

.
.
.
五、線程同步/多線程安全
一般情況下,多線程之間各做各的,沒什么沖突和影響。
多線程安全問題的產(chǎn)生
當(dāng)我們多個(gè)線程訪問同一個(gè)共享數(shù)據(jù),很可能由于一個(gè)線程操作了共享數(shù)據(jù),還沒有語句還沒執(zhí)行完,另一個(gè)線程被cpu調(diào)度又被執(zhí)行也操作了共享數(shù)據(jù)。導(dǎo)致共享數(shù)據(jù)的錯(cuò)誤。
多線程安全問題的原因
1、多個(gè)線程訪問出現(xiàn)延遲。
2、線程隨機(jī)性。
3、操作了共享數(shù)據(jù),這個(gè)是核心
多線程安全問題的解決
利用線程鎖來解決。
對(duì)多條操作共享數(shù)據(jù)的語句,只能讓一個(gè)線程都執(zhí)行完。在執(zhí)行過程中,其他線程不可以參與執(zhí)行。
五.1、多線程問題的產(chǎn)生
我們通過一個(gè)經(jīng)典的賣票代碼來演示多線程安全問題的產(chǎn)生
.
.
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號(hào)窗口").start();
new Thread(sell, "2號(hào)窗口").start();
new Thread(sell, "3號(hào)窗口").start();
}
}
class SellRunnable implements Runnable {
private int num = 10;
@Override
public void run() {
while (true) {
if (num > 0) {
try {
// 通過 Thread.sleep(10) 的延遲可以更好地模擬演示多線程安全問題
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣出第" + num-- + "張票!");
}
}
}
}
.
.
打印結(jié)果
1號(hào)窗口賣出第8張票!
2號(hào)窗口賣出第10張票!
3號(hào)窗口賣出第9張票!
1號(hào)窗口賣出第7張票!
2號(hào)窗口賣出第6張票!
3號(hào)窗口賣出第6張票!
1號(hào)窗口賣出第5張票!
3號(hào)窗口賣出第4張票!
2號(hào)窗口賣出第4張票!
2號(hào)窗口賣出第3張票!
3號(hào)窗口賣出第2張票!
1號(hào)窗口賣出第2張票!
1號(hào)窗口賣出第0張票!
3號(hào)窗口賣出第1張票!
2號(hào)窗口賣出第-1張票!
(打印結(jié)果有可能出現(xiàn)多線程問題有可能不會(huì),多試幾次總會(huì)看到問題)
如上結(jié)果,最直接的我們看到賣出了 -1 張票,這肯定不合邏輯
還有就是有的票重復(fù)賣出,比如第6張,第4張。
還有其他明顯的問題。
就這樣,多線程安全問題產(chǎn)生了。
至于原因,我們已經(jīng)說過了,就是甲線程被調(diào)度,操作還沒結(jié)束,乙線程又被調(diào)度,兩者都操作同一個(gè)數(shù)據(jù)。
五.2、 多線程安全問題的解決
五.2.1、同步代碼塊
格式
synchronized(obj)
{
//obj表示同步監(jiān)視器,是同一個(gè)同步對(duì)象
/**.....
TODO SOMETHING
*/
}
解決示例
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號(hào)窗口").start();
new Thread(sell, "2號(hào)窗口").start();
new Thread(sell, "3號(hào)窗口").start();
}
}
class SellRunnable implements Runnable {
private int num = 10;
String str = "";// 這句代碼的位置很重要,如果放在run里面,那么synchronized (str)的obj就是不同的對(duì)象,會(huì)產(chǎn)生不同的監(jiān)視器
@Override
public void run() {
while (true) {
synchronized (str) {
if (num > 0) {
try {
// 通過 Thread.sleep(1000) 在同步代碼塊更好的模擬不同窗口買票的情況
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣出第" + num-- + "張票!");
}
}
}
}
}
.
.
輸出結(jié)果
1號(hào)窗口賣出第10張票!
1號(hào)窗口賣出第9張票!
1號(hào)窗口賣出第8張票!
1號(hào)窗口賣出第7張票!
3號(hào)窗口賣出第6張票!
3號(hào)窗口賣出第5張票!
1號(hào)窗口賣出第4張票!
1號(hào)窗口賣出第3張票!
2號(hào)窗口賣出第2張票!
1號(hào)窗口賣出第1張票!
如上,利用了同步代碼塊解決了問題,這是線程安全的。
注意點(diǎn):
1、這種打印情況不好復(fù)現(xiàn),有很大可能出現(xiàn)所有票都是 1號(hào)窗口 賣出的情況(如果sleep的時(shí)間是10毫秒就更難復(fù)現(xiàn)了,所以我們sleep調(diào)為1000),如果想更好地復(fù)現(xiàn),我們可以總票數(shù)調(diào)為100張,多線程賣票就可以很好打印出結(jié)果。
2、synchronized (obj)傳入的實(shí)參必須是一個(gè)唯一的對(duì)象,這樣不同的線程才是同一個(gè)面向同于個(gè)監(jiān)視器,才能同步,如果監(jiān)視器不一樣,就談不上同步了。
.
.
你必須知道的synchronized(obj)
簡單理解版
任意類型的對(duì)象都有一個(gè)標(biāo)志位,該標(biāo)志位具有0、1 兩種狀態(tài),其開始狀態(tài)為1。
當(dāng)執(zhí)行synchronized(object)語句后,object對(duì)象的標(biāo)志位變?yōu)?狀態(tài),直到執(zhí)行完整個(gè)synchronized語句中的代碼塊后又回到1狀態(tài)。
一個(gè)線程執(zhí)行到synchronized(object)語句處時(shí),先檢查object對(duì)象的標(biāo)志位,如果為0狀態(tài),表明已經(jīng)有另外的線程的執(zhí)行狀態(tài)正在有關(guān)的同步代碼塊中,這個(gè)線程將暫時(shí)阻塞,讓出CPU資源,直到另外的線程執(zhí)行完有關(guān)的同步代碼塊,將object 對(duì)象的標(biāo)志位恢復(fù)到1狀態(tài),這個(gè)阻塞就被取消,線程能夠繼續(xù)往下執(zhí)行,并將object 對(duì)象的標(biāo)志位變?yōu)?狀態(tài),防止其他線程再進(jìn)入有關(guān)的同步代碼塊中。
如果有多個(gè)線程因等待同一對(duì)象的標(biāo)志位而處于阻塞狀態(tài)時(shí),當(dāng)對(duì)象的標(biāo)志位恢復(fù)到1狀態(tài)時(shí),只會(huì)有一個(gè)線程能夠繼續(xù)運(yùn)行,其他線程仍然處于阻塞等待狀態(tài)。
我們反復(fù)提到有關(guān)的同步代碼塊,是指不僅同一個(gè)代碼塊在多個(gè)線程間可以實(shí)現(xiàn)同步(像上面例子一樣),若干個(gè)不同的代碼塊也可以實(shí)現(xiàn)相互之間的同步,只要各synchronized(object)語句中的object完全是同一個(gè)對(duì)象就可以。
畫張圖吧

原理版
當(dāng)線程執(zhí)行到synchronized的時(shí)候檢查傳入的實(shí)參對(duì)象,并嘗試得到該對(duì)象的鎖旗標(biāo)(就是我們上面講的標(biāo)志位)。
如果得不到,那么此線程就會(huì)被加入到一個(gè)與該對(duì)象的鎖旗標(biāo)相關(guān)連的等待線程池中,一直等到該對(duì)象的鎖旗標(biāo)被歸還,池中的等待線程就可能會(huì)得到該旗標(biāo),然后繼續(xù)執(zhí)行下去。
當(dāng)線程執(zhí)行完成同步代碼塊時(shí),就會(huì)自動(dòng)釋放它占有的同步對(duì)象的鎖旗標(biāo)。一個(gè)用于synchronized語句中的對(duì)象稱為一個(gè)監(jiān)視器,當(dāng)一個(gè)線程獲得了synchronized(object)語句中的代碼塊的執(zhí)行權(quán),即意味著它鎖定了監(jiān)視器,在一段時(shí)間內(nèi),只能有一個(gè)線程可以鎖定監(jiān)視器。
所有其他的線程在試圖進(jìn)入已鎖定的監(jiān)視器時(shí)將被掛起,直到鎖定了監(jiān)視器的線程執(zhí)行完synchronized(object)語句中的代碼塊,即監(jiān)視器被解鎖為止,另外的線程才可以進(jìn)入并鎖定監(jiān)視器。
一個(gè)剛鎖定了監(jiān)視器的線程在監(jiān)視器被解鎖后可以再次進(jìn)入并鎖定同一監(jiān)視器,好比籃球運(yùn)動(dòng)員的籃球出手后可以再次去搶回來一樣。另外當(dāng)在同步塊中遇到break語句或扔出異常時(shí),線程也會(huì)釋放該鎖旗標(biāo)。
其實(shí),程序并不能控制CPU的切換,程序是不可能抱著CPU的大腿不讓他走的。當(dāng)CPU進(jìn)入了一段同步代碼塊中執(zhí)行,CPU是可以切換到其他線程的,只是在準(zhǔn)備執(zhí)行其他線程的代碼時(shí),發(fā)現(xiàn)其他線程處于阻塞狀態(tài),CPU又會(huì)回到先前的線程上。大家也看到同步處理后,程序的運(yùn)行速度比原來沒有使用同步處理前更慢了,因?yàn)橄到y(tǒng)要不停地對(duì)同步監(jiān)視器進(jìn)行檢查,需要更多的開銷。同步是以犧牲程序的性能為代價(jià)的,如果我們能夠確定程序沒有安全性的問題,就沒必要使用同步控制。
小結(jié)
1、synchronized (obj)傳入的實(shí)參必須是同一個(gè)對(duì)象,可以是一個(gè)字符串對(duì)象,可以傳入this用當(dāng)前類來表示這個(gè)對(duì)象等,但是不可以在run里面new出對(duì)象,也就是要確保對(duì)象的唯一性。
2、synchronized (obj)傳入的實(shí)參對(duì)象就是一個(gè)鎖旗幟,(鎖旗幟為了方便理解也可以認(rèn)為是一個(gè)標(biāo)志位)
3、一個(gè)線程對(duì)象嘗試獲取鎖旗幟,如果能獲取那么就順利執(zhí)行邏輯,如果獲取不到就會(huì)被放進(jìn) 等待線程池 ,被掛起,處于阻塞狀態(tài)
4、同步代碼塊中,一個(gè)鎖旗幟只能由于一個(gè)線程對(duì)象所持有。
5、一個(gè)剛鎖定了監(jiān)視器的線程在監(jiān)視器被解鎖后可以再次進(jìn)入并鎖定同一監(jiān)視器,不是說解鎖之后就一定是其他線程獲得鎖旗幟。
6、當(dāng)CPU進(jìn)入了一段同步代碼塊中執(zhí)行,CPU是可以切換到其他線程的,只是在準(zhǔn)備執(zhí)行其他線程的代碼時(shí),發(fā)現(xiàn)其他線程處于阻塞狀態(tài),CPU又會(huì)回到先前的線程上當(dāng)CPU進(jìn)入了一段同步代碼塊中執(zhí)行,CPU是可以切換到其他線程的,只是在準(zhǔn)備執(zhí)行其他線程的代碼時(shí),發(fā)現(xiàn)其他線程處于阻塞狀態(tài),CPU又會(huì)回到先前的線程上
7、多線程同步會(huì)更多地消耗cpu的性能,如果可以確定沒有線程安全問題,多線程沒必要進(jìn)行同步。
.
.
.
五.2.2、 同步函數(shù)/同步方法
格式
在方法上加上synchronized修飾符即可。(一般寫在run方法之外?。?/p>
synchronized 返回值類型 方法名(參數(shù)列表)
{
/**.....
TODO SOMETHING
*/
}
同步方法的同步監(jiān)聽器其實(shí)的是 this
(因?yàn)閟tatic不能調(diào)用this,因此如果是靜態(tài)同步方法,默認(rèn)監(jiān)聽器是當(dāng)前方法所在類的.class對(duì)象)
同步方法 示例
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號(hào)窗口").start();
new Thread(sell, "2號(hào)窗口").start();
new Thread(sell, "3號(hào)窗口").start();
}
}
class SellRunnable implements Runnable {
private int num = 10;
String str = "";
@Override
public void run() {
while (true) {
sellTicket();
}
}
public synchronized void sellTicket(){
if (num > 0) {
try {
// 通過 Thread.sleep(1000) 在同步代碼塊更好的模擬不同窗口買票的情況
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣出第" + num-- + "張票!");
}
}
}
可見,在函數(shù)定義前使用synchronized 關(guān)鍵字也能夠很好實(shí)現(xiàn)線程間的同步。
當(dāng)有一個(gè)線程進(jìn)入了synchronized 修飾的方法(獲得監(jiān)視器),其他線程就不能進(jìn)入同一個(gè)對(duì)象的所有使用了synchronized 修飾的方法, 直到第一個(gè)線程執(zhí)行完它所進(jìn)入的synchronized 修飾的方法為止(離開監(jiān)視器)。
.
.
實(shí)現(xiàn)代碼塊與函數(shù)之間的可以實(shí)現(xiàn)同步
實(shí)現(xiàn)代碼塊與函數(shù)之間的可以實(shí)現(xiàn)同步,前提是兩者使用的必須是同一個(gè)監(jiān)視器。
五.2.3、 同步鎖 ReentrantLock
Lock是java.util.concurrent.locks包下的接口,ReentrantLock類是唯一一個(gè)Lock接口的實(shí)現(xiàn)類,它的意思是可重入鎖,我們這里先演示簡單實(shí)用,后面會(huì)有專門的章節(jié)談?wù)揕ock。
Java類庫中為我們提供了能夠給臨界區(qū)“上鎖”的ReentrantLock類,它實(shí)現(xiàn)了Lock接口。
關(guān)于“臨界區(qū)”,后文我們會(huì)涉及。
示例代碼
import java.util.concurrent.locks.ReentrantLock;
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號(hào)窗口").start();
new Thread(sell, "2號(hào)窗口").start();
new Thread(sell, "3號(hào)窗口").start();
}
}
class SellRunnable implements Runnable {
private int num = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
sell();
}
}
public void sell(){
lock.lock(); // 上鎖
try{
if (num > 0) {
try {
// 通過 Thread.sleep(1000) 在同步代碼塊更好的模擬不同窗口買票的情況
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣出第" + num-- + "張票!");
}
}finally {
lock.unlock(); // 解鎖
}
}
}
用法和同步方法類似,注意上鎖和手動(dòng)解鎖。
.
.
輸出依然沒有安全問題的產(chǎn)生。
六、競爭條件與臨界區(qū)
在同一程序中運(yùn)行多個(gè)線程本身不會(huì)導(dǎo)致問題,問題在于多個(gè)線程訪問了相同的資源。如,同一內(nèi)存區(qū)(變量,數(shù)組,或?qū)ο螅?、系統(tǒng)(數(shù)據(jù)庫,web services 等)或文件。實(shí)際上,這些問題只有在一或多個(gè)線程向這些資源做了寫操作時(shí)才有可能發(fā)生,只要資源沒有發(fā)生變化,多個(gè)線程讀取相同的資源就是安全的。
多線程同時(shí)執(zhí)行下面的代碼可能會(huì)出錯(cuò):
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
想象下線程 A 和 B 同時(shí)執(zhí)行同一個(gè) Counter 對(duì)象的 add()方法,我們無法知道操作系統(tǒng)何時(shí)會(huì)在兩個(gè)線程之間切換。JVM 并不是將這段代碼視為單條指令來執(zhí)行的,而是按照下面的順序:
從內(nèi)存獲取 this.count 的值放到寄存器
將寄存器中的值增加 value
將寄存器中的值寫回內(nèi)存
觀察線程 A 和 B 交錯(cuò)執(zhí)行會(huì)發(fā)生什么:
this.count = 0;
A: 讀取 this.count 到一個(gè)寄存器 (0)
B: 讀取 this.count 到一個(gè)寄存器 (0)
B: 將寄存器的值加 2
B: 回寫寄存器值(2)到內(nèi)存. this.count 現(xiàn)在等于 2
A: 將寄存器的值加 3
A: 回寫寄存器值(3)到內(nèi)存. this.count 現(xiàn)在等于 3
兩個(gè)線程分別加了 2 和 3 到 count 變量上,兩個(gè)線程執(zhí)行結(jié)束后 count 變量的值應(yīng)該等于 5。然而由于兩個(gè)線程是交叉執(zhí)行的,兩個(gè)線程從內(nèi)存中讀出的初始值都是 0。然后各自加了 2 和 3,并分別寫回內(nèi)存。最終的值并不是期望的 5,而是最后寫回內(nèi)存的那個(gè)線程的值,上面例子中最后寫回內(nèi)存的是線程 A,但實(shí)際中也可能是線程 B。如果沒有采用合適的同步機(jī)制,線程間的交叉執(zhí)行情況就無法預(yù)料。
競爭條件(race condition)
當(dāng)兩個(gè)線程競爭同一資源時(shí),如果對(duì)資源的訪問順序敏感,就稱存在競態(tài)條件。
臨界區(qū)(critical area)
導(dǎo)致競態(tài)條件發(fā)生的代碼區(qū)稱作臨界區(qū)。
上例中 add()方法就是一個(gè)臨界區(qū),它會(huì)產(chǎn)生競態(tài)條件。在臨界區(qū)中使用適當(dāng)?shù)耐骄涂梢员苊飧倯B(tài)條件。
七、Lock與synchronized
七.1、Lock與synchronized
Java中可以使用 Lock 和 synchronized 都可以實(shí)現(xiàn)對(duì)某個(gè)共享資源的同步,同時(shí)也可以實(shí)現(xiàn)對(duì)某些過程的原子性操作。Lock內(nèi)部也使用了synchronized。
通常用法
synchronized:
在需要同步的對(duì)象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號(hào)中表示需要鎖的對(duì)象。Lock:
需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個(gè)線程中必須要使用一個(gè)ReentrantLock類做為對(duì)象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會(huì)在finally塊中寫unlock()以防死鎖。
性能上的一點(diǎn)事
在 JDK1.5 中,synchronized 是性能低效的。因?yàn)檫@是一個(gè)重量級(jí)操作,它對(duì)性能最大的影響是阻塞的是實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java 提供的 Lock 對(duì)象,性能更高一些。Brian Goetz 對(duì)這兩種鎖在 JDK1.5、單核處理器及雙 Xeon 處理器環(huán)境下做了一組吞吐量對(duì)比的實(shí)驗(yàn),發(fā)現(xiàn)多線程環(huán)境下,synchronized的吞吐量下降的非常嚴(yán)重,而ReentrankLock 則能基本保持在同一個(gè)比較穩(wěn)定的水平上。但與其說 ReetrantLock 性能好,倒不如說 synchronized 還有非常大的優(yōu)化余地。
于是到了 JDK1.6,發(fā)生了變化,對(duì) synchronize 加入了很多優(yōu)化措施,有自適應(yīng)自旋,鎖消除,鎖粗化,輕量級(jí)鎖,偏向鎖等等。導(dǎo)致在 JDK1.6 上 synchronize 的性能并不比 Lock 差。官方也表示,他們也更支持 synchronize,在未來的版本中還有優(yōu)化余地,所以還是提倡在 synchronized 能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用 synchronized 來進(jìn)行同步。
Lock可以使用Condition進(jìn)行線程之間的調(diào)度,Synchronized則使用Object對(duì)象本身的notify, wait, notityAll調(diào)度機(jī)制,這兩種調(diào)度機(jī)制有什么異同呢?
Condition是Java5以后出現(xiàn)的機(jī)制,它有更好的靈活性,而且在一個(gè)對(duì)象里面可以有多個(gè)Condition(即對(duì)象監(jiān)視器),線程可以注冊(cè)在不同的Condition,從而可以有選擇性的調(diào)度線程,更加靈活。
Synchronized就相當(dāng)于整個(gè)對(duì)象只有一個(gè)單一的Condition(即該對(duì)象本身)所有的線程都注冊(cè)在它身上,線程調(diào)度的時(shí)候之后調(diào)度所有得注冊(cè)線程,沒有選擇權(quán),會(huì)出現(xiàn)相當(dāng)大的問題 。
七.2、Lock
七.2.1 Lock接口和方法
Java 5 中引入了新的鎖機(jī)制——java.util.concurrent.locks 中的顯式的互斥鎖:Lock 接口,它提供了比synchronized 更加廣泛的鎖定操作。
先來看一下Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
方法介紹
- lock方法:用來獲取鎖,在鎖被占用時(shí)它會(huì)一直阻塞,并且這個(gè)方法不能被中斷;
- lockInterruptibly方法:在獲取不到鎖時(shí)也會(huì)阻塞,它與lock方法的區(qū)別在于阻塞在該方法時(shí)可以被中斷;
- tryLock方法:也是用來獲取鎖的,它的無參版本在獲取不到鎖時(shí)會(huì)立刻返回false,它的計(jì)時(shí)等待版本會(huì)在等待指定時(shí)間還獲取不到鎖時(shí)返回false,計(jì)時(shí)等待的tryLock在阻塞期間也能夠被中斷。使用tryLock方法的典型代碼如下:
if (myLock.tryLock()) {
try {
…
} finally {
myLock.unlock();
}
} else {
//做其他的工作
}
unlock方法:用來釋放鎖;
newCondition方法:用來獲取當(dāng)前鎖對(duì)象相關(guān)的條件對(duì)象。
七.2.2 關(guān)于鎖的一些概念
可重入鎖
可重入鎖的概念是自己可以再次獲取自己的內(nèi)部鎖。舉個(gè)例子,比如一條線程獲得了某個(gè)對(duì)象的鎖,此時(shí)這個(gè)對(duì)象鎖還沒有釋放,當(dāng)其再次想要獲取這個(gè)對(duì)象的鎖的時(shí)候還是可以獲取的(如果不可重入的鎖的話,此刻會(huì)造成死鎖)。說的更高深一點(diǎn)可重入鎖是一種遞歸無阻塞的同步機(jī)制。
讀寫鎖
讀寫鎖拆成讀鎖和寫鎖來理解。讀鎖可以共享,多個(gè)線程可以同時(shí)擁有讀鎖,但是寫鎖卻只能只有一個(gè)線程擁有,而且獲取寫鎖的時(shí)候其他線程都已經(jīng)釋放了讀鎖,而且該線程獲取寫鎖之后,其他線程不能再獲取讀鎖。
簡單的說就是寫鎖是排他鎖,讀鎖是共享鎖。
公平鎖和非公平鎖
獲取鎖涉及到的兩個(gè)概念即 公平和非公平
公平鎖
公平表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO順序。非公平鎖
非公平就是一種獲取鎖的搶占機(jī)制,和公平相對(duì)就是先來不一定先得,這個(gè)方式可能造成某些線程饑餓(一直拿不到鎖)。
關(guān)于鎖頭的其他詳細(xì)分類可以查看 Java鎖的種類以及辨析
Lock 接口有 3 個(gè)實(shí)現(xiàn)它的類
- ReentrantLock 重入鎖
- ReetrantReadWriteLock.ReadLock 讀鎖
- ReetrantReadWriteLock.WriteLock 寫鎖
Lock 必須被顯式地創(chuàng)建、鎖定和釋放,為了可以使用更多的功能,一般用 ReentrantLock 為其實(shí)例化。為了保證鎖最終一定會(huì)被釋放(可能會(huì)有異常發(fā)生),要把互斥區(qū)放在 try 語句塊內(nèi),并在 finally 語句塊中釋放鎖,尤其當(dāng)有 return 語句時(shí),return 語句必須放在 try 字句中,以確保 unlock()不會(huì)過早發(fā)生,從而將數(shù)據(jù)暴露給第二個(gè)任務(wù)。因此,采用 lock 加鎖和釋放鎖的一般形式如下:
Lock lock = new ReentrantLock();//默認(rèn)使用非公平鎖,如果要使用公平鎖,需要傳入?yún)?shù)true
........
lock.lock();
try {
//更新對(duì)象的狀態(tài)
//捕獲異常,必要時(shí)恢復(fù)到原來的不變約束
//如果有return語句,放在這里
finally {
lock.unlock(); //鎖必須在finally塊中釋放
}
關(guān)于線程的同步的先到這里。
八、線程間的通信
線程通信的目標(biāo)是使線程間能夠互相發(fā)送信號(hào)。另一方面,線程通信使線程能夠等待其他線程的信號(hào)。
我們用一個(gè)經(jīng)典的生產(chǎn)者和消費(fèi)者的代碼來演示這個(gè)線程間的通信。
需求:生產(chǎn)者產(chǎn)出一個(gè)產(chǎn)品,消費(fèi)者就消費(fèi)一個(gè)產(chǎn)品
需要控制好通信和線程安全的問題,不能出現(xiàn)
1、產(chǎn)品還沒生產(chǎn)就被消費(fèi)了
2、一個(gè)產(chǎn)品被多次重復(fù)消費(fèi)
3、不能生產(chǎn)了一批就開始消費(fèi)(要求生產(chǎn)一個(gè)就馬上消費(fèi)一個(gè))
涉及到進(jìn)程間通訊的幾個(gè)方法
寫代碼之前,我們先來看一下幾個(gè)方法
wait()
讓當(dāng)前線程放棄監(jiān)視器進(jìn)入等待,直到其他線程調(diào)用同一個(gè)監(jiān)視器并調(diào)用notify()或notifyAll()為止。notify()
喚醒在同一對(duì)象監(jiān)聽器中調(diào)用wait方法的第一個(gè)線程。notifyAll()
喚醒在同一對(duì)象監(jiān)聽器中調(diào)用wait方法的所有線程。
wait()、notify()、notifyAll()的調(diào)用細(xì)節(jié)
wait()、notify()、notifyAll(),這三個(gè)方法屬于Object 不屬于 Thread
這三個(gè)方法必須由同步監(jiān)視對(duì)象來調(diào)用,兩種情況:
- 1.synchronized修飾的方法,因?yàn)樵擃惖哪J(rèn)實(shí)例(this)就是同步監(jiān)視器,所以可以在同步方法中調(diào)用這三個(gè)方法;
- 2.synchronized修飾的同步代碼塊,同步監(jiān)視器是括號(hào)里的對(duì)象,所以必須使用該對(duì)象調(diào)用這三個(gè)方法;
可要是我們使用的是Lock對(duì)象來保證同步的,系統(tǒng)中不存在隱式的同步監(jiān)視器對(duì)象,那么就不能使用者三個(gè)方法了,那該咋辦呢?
此時(shí),Lock代替了同步方法或同步代碼塊,Condition代替了同步監(jiān)視器的功能;
Condition對(duì)象通過Lock對(duì)象的newCondition()方法創(chuàng)建;
里面方法包括:
- await(): 等價(jià)于同步監(jiān)聽器的wait()方法;
- signal(): 等價(jià)于同步監(jiān)聽器的notify()方法;
- signalAll(): 等價(jià)于同步監(jiān)聽器的notifyAll()方法;
線程間通信的示例
public class TestClass {
public static void main(String[] args) {
Goods g = new Goods();
new Thread(new Producer(g)).start();
new Thread(new Consumer(g)).start();
}
}
class Goods{
private String name;
private String price;
private Boolean isimpty = Boolean.TRUE;//內(nèi)存區(qū)為空!
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String sex) {
this.price = sex;
}
public void setGoods(String name,String sex){
synchronized (this) {
// 生產(chǎn)的產(chǎn)品不為空,還沒被消費(fèi),就不生產(chǎn),放棄監(jiān)視器進(jìn)入等待
while(isimpty.equals(Boolean.FALSE)){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;//為空的話生產(chǎn)者創(chuàng)造!
this.price = sex;
isimpty = Boolean.FALSE;//創(chuàng)造結(jié)束后修改屬性!
System.out.println("生產(chǎn) +++ 產(chǎn)品:"+getName()+ ", "+"價(jià)格:"+getPrice());
this.notifyAll();
}
}
public void getGoods(){
synchronized (this) {
// 生產(chǎn)的產(chǎn)品為空,沒有產(chǎn)品可以被消費(fèi),就不消費(fèi),放棄監(jiān)視器進(jìn)入等待
while(isimpty.equals(Boolean.TRUE)){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費(fèi) --- 產(chǎn)品:"+getName()+ ", "+"價(jià)格:"+getPrice());
isimpty = Boolean.TRUE;
this.notifyAll();
}
}
}
class Producer implements Runnable{
private Goods pgs;
public Producer(Goods p) {
super();
this.pgs = p;
}
@Override
public void run() {
for (int i = 0; i < 12; i++) {
pgs.setGoods("產(chǎn)品編號(hào)為:"+i, i+10+".00");
}
}
}
class Consumer implements Runnable{
private Goods cgs;
public Consumer(Goods p) {
super();
this.cgs = p;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
cgs.getGoods();
}
}
}
如上代碼,
當(dāng)我們生產(chǎn)時(shí)候,
如果發(fā)現(xiàn)已經(jīng)有產(chǎn)品存在沒被消費(fèi),那么這個(gè)生產(chǎn)的線程就wait,不接著往下執(zhí)行生產(chǎn)的代碼了;
如果產(chǎn)品為空,就這順利執(zhí)行生產(chǎn)的代碼,并且調(diào)用 this.notifyAll(); 通知消費(fèi)者線程可以來消費(fèi)了當(dāng)我們消費(fèi)的時(shí)候
如果沒有產(chǎn)品可以被消費(fèi),那么消費(fèi)者線程就wait,不接著往下執(zhí)行消費(fèi)的代碼了;
如果產(chǎn)品不為空,就順利執(zhí)行消費(fèi)的代碼,消費(fèi)完成之后調(diào)用 this.notifyAll();喚醒之前因?yàn)樯a(chǎn)完被職位wait的生產(chǎn)者線程。
.
.
輸出
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:0, 價(jià)格:10.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:0, 價(jià)格:10.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:1, 價(jià)格:11.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:1, 價(jià)格:11.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:2, 價(jià)格:12.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:2, 價(jià)格:12.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:3, 價(jià)格:13.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:3, 價(jià)格:13.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:4, 價(jià)格:14.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:4, 價(jià)格:14.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:5, 價(jià)格:15.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:5, 價(jià)格:15.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:6, 價(jià)格:16.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:6, 價(jià)格:16.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:7, 價(jià)格:17.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:7, 價(jià)格:17.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:8, 價(jià)格:18.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:8, 價(jià)格:18.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:9, 價(jià)格:19.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:9, 價(jià)格:19.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:10, 價(jià)格:20.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:10, 價(jià)格:20.00
生產(chǎn) +++ 產(chǎn)品:產(chǎn)品編號(hào)為:11, 價(jià)格:21.00
消費(fèi) --- 產(chǎn)品:產(chǎn)品編號(hào)為:11, 價(jià)格:21.00
利用 代碼展示線程狀態(tài)
參考:
Java核心技術(shù)點(diǎn)之多線程
Java并發(fā)編程:Thread類的使用
java 線程的優(yōu)先級(jí)Priority