一、進程和線程
進程
進程就是一個執行中的程序實例,每個進程都有自己獨立的一塊內存空間,一個進程中可以有多個線程。比如在Windows系統中,一個運行的xx.exe就是一個進程。
- 每個進程有各自獨立的一塊內存,使得各個進程之間內存地址相互隔離。
線程
線程是指進程中的一個執行任務(控制單元),一個進程中可以運行多個線程。
同一個進程間的多個線程共享該進程的數據,
通常情況下:
1、多線程在數據共享上要比多進程更加便捷。
2、一個進程使用多線程,通過提高cpu使用率可以提高效率,因為多線程可以有效的使用系統的資源和提高系統的吞吐量(單位時間內執行的指令數)
線程本身本身并不能提高效率,是曲線救國通過提高資源使用效率來提高系統的效率
3、 Java程序的進程里至少有這么幾個線程:主線程, 垃圾回收線程(后臺線程)
4、因為CPU在瞬間不斷切換去處理各個線程,導致了多線程的執行具有隨機性
.
.
一個標準的線程,由線程ID、當前指令指針(PC)、寄存器和堆棧組成。
一個標準的進程,由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。
—— [ 編程思想之多線程與多進程(1)——以操作系統的角度述說線程與進程 ]
二、并發和并行
- 單核cpu,并發
- 多核cpu,并行
單核并發
在單核機器上,“多進程”并不是真正的多個進程在同時執行,而是通過CPU時間分片,操作系統快速在進程間切換而模擬出來的多進程。我們通常把這種情況成為并發。
單核的多個進程,不是“一并發生”的,是cpu高速切換讓我們看起來像“一并發生”而已。
多核并行
我們使用的計算機基本上都搭載了多核CPU,這時,我們能真正的實現多個進程并行執行,這種情況叫做并行(一并進行)。
多核cpu讓多進程的并發有可能變成了并行。
所以我們說得并發,有可能是是并行,也可能是并發,這跟cpu的核心數有關系。
在多核機器上,我們的多個線程可以并行執行在多個核上,進一步提升效率。
例子
舉一個并發的小例子,多線程下載文件。
比如我們有一個9m的文件要下載,使用3個線程來下載,那么每個線程下載的分到現在大小為3m。
一方面,因為線程搶占cpu具有隨機性,多線程更加容易搶占到被cpu執行機會。
另外一方面,并行/并行工作,會單線程快一些。
三、線程的創建方式
Java的線程有2種創建方式
第一種,繼承Thread類
直接定義一個Thread的子類并實例化,從而創建一個新線程。通過start創建線程
class MyThread extends Thread {
public void run() {
//這里是線程要執行的任務
}
}
直接對其調用start方法,即可啟動這個線程:
t.start();
第二種方式,實現 Runnable 接口,
實現 Runnable 接口,把 Runnable 作為參數傳入 Thread 的構造函數,通過start創建線程
class MyRunnable implements Runnable {
...
public void run() {
//這里是新線程需要執行的任務
}
}
Runnable r = new MyRunnable();
Thread t = new Thread(r);
調用start方法,即可啟動這個線程:
t.start();
.
.
來一個例子吧
線程的啟動
public class TestClass {
public static void main(String[] args) {
System.out.println("main 當前線程:"+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方法執行");
System.out.println("MyThread run 當前線程 "+Thread.currentThread().getName());
System.out.println("========");
}
}
class RunThread implements Runnable{
@Override
public void run() {
System.out.println("RunThread run方法執行");
System.out.println("RunThread run 當前線程 "+Thread.currentThread().getName());
System.out.println("========");
}
}
.
打印
main 當前線程:main
========
MyThread run方法執行
MyThread run 當前線程 Thread-0
========
RunThread run方法執行
RunThread run 當前線程 Thread-1
========
注:+Thread.currentThread().getName()+Thread.currentThread().getName() 可以獲得當前線程的名稱
.
.
從上面的代碼中,雖然看起來很有規律,我們知道線程不是一旦start啟動就會被執行,很可能有個等待的過程,被cpu隨機切換才正式工作,工作有可能隨時被打斷,后面我們會再看,現在先來一份簡單示例
.
.
線程的被cpu調度具有隨機性
public class TestClass {
public static void main(String[] args) {
for(int y = 0;y<6;y++){
new Thread(new RunThread()).start();
System.out.println("啟動 第"+ y +"個線程");
}
}
}
class RunThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println("run work"+i);
}
}
}
.
.
打印
啟動 第0個線程
run work0
run work1
run work2
啟動 第1個線程
run work0
run work1
啟動 第2個線程
run work2
run work0
啟動 第3個線程
run work1
run work2
run work0
啟動 第4個線程
run work1
run work0
run work2
啟動 第5個線程
run work0
run work1
run work1
run work2
run work2
1、打印結果不唯一,隨機
2、繼承自Thread和實現Runnable都一樣,就不另附代碼了
.
.
為線程指定名稱
主線程默認名稱為 main
其他線程默認的名稱是 Thread-數字,但是我們可以通過給寫一個構造函數,在構造函數里面出入字符串給super的方式給線程指定名稱。
我們再來看一份代碼。
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 張三
.
.
兩種創建方式對比
**對于第一種方式,繼承Thread類 **
A extends Thread:
- 同份資源不共享
- 無法繼承其他類了
**對于第二種方式,實現 Runnable 接口作為Thread類構造函數 **
class MyRunnable implements Runnable
- 多個線程共享一個目標資源,適合多線程處理同一份資源。
- 該類還可以繼承其他類
第二種相對比較推薦。
關于run和statr方法
start()
start()方法的作用是啟動一個新線程,新線程會執行相應的run()方法。start()不能被重復調用。
run()
run()就和普通的成員方法一樣,可以被重復調用。單獨調用run()的話,會在當前線程中執行run(),而并不會啟動新線程!
三、線程的生命周期/狀態
Thread類內部有個public的枚舉Thread.State,里邊將線程的狀態分為:
New(新生)
NEW(新建尚未運行/啟動)
Runnable(可運行)
處于可運行狀態:正在運行或準備運行
在線程對象上調用start方法后,相應線程便會進入Runnable狀態,若被線程調度程序調度,這個線程便會成為當前運行(Running)的線程;
Blocked(被阻塞)
阻塞狀態,受阻塞并等待某個監視器鎖的線程,處于這種狀態。
若一段代碼被線程A “上鎖” ,此時線程B嘗試執行這段代碼,線程B就會進入Blocked狀態;
Waiting(等待)
通過wait方法進入的等待
當線程等待另一個線程通知線程調度器一個條件時,它本身就會進入Waiting狀態;
Time Waiting(計時等待)
通過sleep或wait timeout方法進入的限期等待的狀態
計時等待與等待的區別是,線程只等待一定的時間,若超時則不再等待;
Terminated(被終止)
線程終止狀態
線程的run方法執行完畢或者由于一個未捕獲的異常導致run方法意外終止會進入Terminated狀態。
口頭的“阻塞”統一指代Blocked、Waiting、Time Waiting其中任一。
來個簡圖
四、線程的基本方法/控制線程
要了解的有如下方法
- start()
- Thread.sleep()
- interrupt
- isAlive()
- join()
- yield()
- wait()
- **notify() 和 notifyAll() **
- getPriority() 和 setPriority()
(下文涉及到多線程的代碼運行輸出部分,結果并不是一定是唯一的,因為cpu調度線程是隨機的)
四.1 start()
這是一個實例方法,啟動一個線程,但是線程不是已啟動就會執行
四.2 isAlive()
這是一個實例方法, 用于判斷當前線程是否還“活著”,即線程是否終止,返回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本身是 中斷,打斷 的意思
這是一個實例方法。每個線程都有一個 中斷狀態 標識,如果調用 interrupt 會將相應線程的 中斷狀態 標記為 true,再通過 isInterrupted() 方法即可獲得 中斷標志 為true。
調用interrupt,能夠 打斷 那些通過調用 可中斷方法 進入 阻塞狀態 的線程。
常見的可中斷方法有sleep、wait、join,這些方法的內部實現會時不時的檢查當前線程的中斷狀態,若為true會立刻拋出一個InterruptedException異常,從而中斷當前線程。
中斷捕獲異常后,是決定讓線程繼續運行,還是結束等要根據業務場景才處理。
注:注意,如果線程從來沒有調用 可中斷方法 ,然后我們就去調用 interrupt 那么線程是不會被中斷的。interrupt能夠讓線程中斷的核心原因是sleep、wait、join等可中斷方法的內部檢查到interrupt狀態位true拋異常造成的。
使用 interrupt 來停止線程是比較粗暴的(interrupt并不能讓線程終止),還有另外一個方法,stop()方法(stop會直接線程終止),這個方法已經被棄用,這個方法更加粗暴。應該怎么合理停止線程我們后面會涉及到。
待會我們會結合sleep方法和interrupt方法寫一份小demo
四.4 Thread.sleep()
給當前線程指定睡眠毫秒值
結合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();不會被執行
// 主線程睡眠期間子線程myThread愉快地每隔一秒打印一次時間
Thread.sleep(3000);
}catch (InterruptedException e) {
}
// 主線程3秒過后,接著工作,myThread.interrupt()被執行,myThread被終端,不再打印時間
// 我們這里myThread.interrupt();可以順利中斷myThread是因為myThread之前調用了可中斷方法sleep
myThread.interrupt();
System.out.println("=== myThread.interrupt() 被執行 ===");
}
}
class MyThread extends Thread {
boolean flag = true;
public void run() {
// 正常來說,不被影響的下面這段代碼會1秒打印一次當前時間
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() 被執行 ===
===捕獲 InterruptedException ===
可見,myThread.interrupt()成功地中斷了myThread線程。
.
.
四.5 join()
這是一個實例方法,在A線程中對線程B調用join方法會導致A線程暫時處于waiting,等線程B運行完畢后再接著運行線程A。
也就是說,把當前線程還沒執行的部分“接到”另一個線程后面去,另一個線程運行完畢后,當前線程再接著運行。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;
無參數的join表示當前線程一直等到另一個線程運行完畢,這種情況下當前線程會處于Wating狀態;
帶參數的表示當前線程只等待指定的時間,這種情況下當前線程會處于Time Waiting狀態。當前線程通過調用join方法進入Time Waiting或Waiting狀態后,會釋放已經獲取的鎖。實際上,join方法內部調用了Object類的實例方法wait
join改變線程執行結果
示例代碼
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("主線程 的狀態(子線程join之前) "+Thread.currentThread().getState());
// 子線程 join,主線程停止等待
try {
myThread.join();
System.out.println("主線程 的狀態(子線程join之后) "+Thread.currentThread().getState());
}catch (InterruptedException e) {
System.out.println("===主線程 捕獲 InterruptedException ===");
}
for(int i=0;i<8;i++){
System.out.println("主線程for循環 "+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
主線程 的狀態(子線程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===
主線程 的狀態(子線程join之后) RUNNABLE
主線程for循環 main == 0
主線程for循環 main == 1
主線程for循環 main == 2
主線程for循環 main == 3
主線程for循環 main == 4
主線程for循環 main == 5
主線程for循環 main == 6
主線程for循環 main == 7
可以看到,在myThread.start();執行之后,主線程停止執行,直到子線程的run工作后主線程再繼續工作。
如果我們上面代碼中的
myThread.join();
這行代碼和其try備注掉,那么打印結果很可能就是
主線程工作中 main
主線程 的狀態(子線程join之前) RUNNABLE
主線程for循環 main == 0
主線程for循環 main == 1
主線程for循環 main == 2
主線程for循環 main == 3
主線程for循環 main == 4
主線程for循環 main == 5
主線程for循環 main == 6
主線程for循環 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===
通過這兩段輸出,我們可以看到join的作用了。
還有,子線程調用join時,主線程還是Runnable狀態
四.6 getPriority() 和 setPriority()
getPriority() 獲得線程的優先級數值
setPriority() 設置線程的優先級數值
優先級范圍
線程存在優先級,優先級范圍在1~10之間。
默認優先級和優先級常量
線程默認優先級是5,Thread類中有三個常量,定義線程優先級范圍:
static int MAX_PRIORITY
線程可以具有的最高優先級。
static int MIN_PRIORITY
線程可以具有的最低優先級。
static int NORM_PRIORITY
分配給線程的默認優先級。
JVM線程調度程序是基于優先級的調度機制。在大多數情況下,
1、當前運行的線程優先級將大于或等于線程池中任何線程的優先級。
2、優先級高的線程被cpu調度的概率大于優先級低的線程
1、不是說優先級低在在優先級高的面前就不執行了,只是優先級高的執行頻率比較高,而優先級低的執行一小會就會被趕出來。
2、當設計多線程應用程序的時候,一定不要依賴于線程的優先級。因為線程調度優先級操作是沒有保障的,只能把線程優先級作用作為一種提高程序效率的方法,但是要保證程序不依賴這種操作。
設置優先級
示例代碼
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); //設置優先級的值
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()
這是一個靜態方法,作用是讓當前線程“讓步”,目的是讓其他線程有更大的可能被系統調度,這個方法不會釋放鎖。
yield() 的“讓步”只是讓一小會,一小會之后就接著工作了。
yield操作時,線程還是Runnable狀態。
調用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){
// 當循環到模以3為0的時候,就做出一小會的“讓步”
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類中定義的實例方法。在指定對象上調用wait方法能夠讓當前線程進入阻塞狀態(前提時當前線程持有該對象的內部鎖(monitor)),此時當前線程會釋放已經獲取的那個對象的內部鎖,這樣一來其他線程就可以獲取這個對象的內部鎖了。當其他線程獲取了這個對象的內部鎖,進行了一些操作后可以調用notify方法來喚醒正在等待該對象的線程。
關于wait()涉及到線程鎖和線程通信問題,后文的關于會有相關參考代碼。
.
.
四.9 **notify() 和 notifyAll() **
notify/notifyAll方法也是Object類中定義的實例方法。作用是喚醒正在等待相應對象的線程
notify() 喚醒 wait pool 一個等待該對象的線程
notifyAll() 喚醒 wait pool 所有等待該對象的線程
關于notify()/notifyAll()涉及到線程鎖和線程通信問題,后文的關于會有相關參考代碼。
.
.
四.10 如何停止線程
注意:
1、interrupt 并無法真正停止線程
2、Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 這些終止線程運行的方法已經被廢棄,使用它們是極端不安全的!
正常情況下線程什么時候回停止?
run方法執行完畢,該線程就會正常結束。
(但有時候線程是永遠無法結束的,比如while(true)。)
1、利用run里面的標志位結束線程
run里面while,用boolean標志位控制
代碼
public class TestClass {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
// 延緩一下,不至于線程馬上關閉,為了方便打印演示結果
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() 執行");
}
}
class MyThread extends Thread {
// 停止線程的標志位 (run結束,線程就結束)
private boolean flag = true;
public void run() {
int i = 0;
while (flag == true) {
System.out.println("MyThread run " + i++);
}
}
public void shutDown() {
// 自己定義這個方法,調用時讓線程停止
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() 執行
MyThread run 2
.
.
再來一次輸出
in thread main i=0
in thread main i=10
in thread main i=20
myThread.shutDown() 執行
MyThread run 0
可見,停止進程后不是說run里面的代碼就絕對馬上停止,可能還會執行個一次兩次,但是關閉確實是實現的了。
(有時間再補上其他的方式)
待添加
下面這副圖描述了線程從創建到消亡之間的狀態:
.
.
.
五、線程同步/多線程安全
一般情況下,多線程之間各做各的,沒什么沖突和影響。
多線程安全問題的產生
當我們多個線程訪問同一個共享數據,很可能由于一個線程操作了共享數據,還沒有語句還沒執行完,另一個線程被cpu調度又被執行也操作了共享數據。導致共享數據的錯誤。
多線程安全問題的原因
1、多個線程訪問出現延遲。
2、線程隨機性。
3、操作了共享數據,這個是核心
多線程安全問題的解決
利用線程鎖來解決。
對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。
五.1、多線程問題的產生
我們通過一個經典的賣票代碼來演示多線程安全問題的產生
.
.
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號窗口").start();
new Thread(sell, "2號窗口").start();
new Thread(sell, "3號窗口").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-- + "張票!");
}
}
}
}
.
.
打印結果
1號窗口賣出第8張票!
2號窗口賣出第10張票!
3號窗口賣出第9張票!
1號窗口賣出第7張票!
2號窗口賣出第6張票!
3號窗口賣出第6張票!
1號窗口賣出第5張票!
3號窗口賣出第4張票!
2號窗口賣出第4張票!
2號窗口賣出第3張票!
3號窗口賣出第2張票!
1號窗口賣出第2張票!
1號窗口賣出第0張票!
3號窗口賣出第1張票!
2號窗口賣出第-1張票!
(打印結果有可能出現多線程問題有可能不會,多試幾次總會看到問題)
如上結果,最直接的我們看到賣出了 -1 張票,這肯定不合邏輯
還有就是有的票重復賣出,比如第6張,第4張。
還有其他明顯的問題。
就這樣,多線程安全問題產生了。
至于原因,我們已經說過了,就是甲線程被調度,操作還沒結束,乙線程又被調度,兩者都操作同一個數據。
五.2、 多線程安全問題的解決
五.2.1、同步代碼塊
格式
synchronized(obj)
{
//obj表示同步監視器,是同一個同步對象
/**.....
TODO SOMETHING
*/
}
解決示例
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號窗口").start();
new Thread(sell, "2號窗口").start();
new Thread(sell, "3號窗口").start();
}
}
class SellRunnable implements Runnable {
private int num = 10;
String str = "";// 這句代碼的位置很重要,如果放在run里面,那么synchronized (str)的obj就是不同的對象,會產生不同的監視器
@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-- + "張票!");
}
}
}
}
}
.
.
輸出結果
1號窗口賣出第10張票!
1號窗口賣出第9張票!
1號窗口賣出第8張票!
1號窗口賣出第7張票!
3號窗口賣出第6張票!
3號窗口賣出第5張票!
1號窗口賣出第4張票!
1號窗口賣出第3張票!
2號窗口賣出第2張票!
1號窗口賣出第1張票!
如上,利用了同步代碼塊解決了問題,這是線程安全的。
注意點:
1、這種打印情況不好復現,有很大可能出現所有票都是 1號窗口 賣出的情況(如果sleep的時間是10毫秒就更難復現了,所以我們sleep調為1000),如果想更好地復現,我們可以總票數調為100張,多線程賣票就可以很好打印出結果。
2、synchronized (obj)傳入的實參必須是一個唯一的對象,這樣不同的線程才是同一個面向同于個監視器,才能同步,如果監視器不一樣,就談不上同步了。
.
.
你必須知道的synchronized(obj)
簡單理解版
任意類型的對象都有一個標志位,該標志位具有0、1 兩種狀態,其開始狀態為1。
當執行synchronized(object)語句后,object對象的標志位變為0狀態,直到執行完整個synchronized語句中的代碼塊后又回到1狀態。
一個線程執行到synchronized(object)語句處時,先檢查object對象的標志位,如果為0狀態,表明已經有另外的線程的執行狀態正在有關的同步代碼塊中,這個線程將暫時阻塞,讓出CPU資源,直到另外的線程執行完有關的同步代碼塊,將object 對象的標志位恢復到1狀態,這個阻塞就被取消,線程能夠繼續往下執行,并將object 對象的標志位變為0狀態,防止其他線程再進入有關的同步代碼塊中。
如果有多個線程因等待同一對象的標志位而處于阻塞狀態時,當對象的標志位恢復到1狀態時,只會有一個線程能夠繼續運行,其他線程仍然處于阻塞等待狀態。
我們反復提到有關的同步代碼塊,是指不僅同一個代碼塊在多個線程間可以實現同步(像上面例子一樣),若干個不同的代碼塊也可以實現相互之間的同步,只要各synchronized(object)語句中的object完全是同一個對象就可以。
畫張圖吧
原理版
當線程執行到synchronized的時候檢查傳入的實參對象,并嘗試得到該對象的鎖旗標
(就是我們上面講的標志位)。
如果得不到,那么此線程就會被加入到一個與該對象的鎖旗標相關連的等待線程池
中,一直等到該對象的鎖旗標被歸還,池中的等待線程就可能會得到該旗標,然后繼續執行下去。
當線程執行完成同步代碼塊時,就會自動釋放它占有的同步對象的鎖旗標。一個用于synchronized語句中的對象稱為一個監視器
,當一個線程獲得了synchronized(object)語句中的代碼塊的執行權,即意味著它鎖定了監視器
,在一段時間內,只能有一個線程可以鎖定監視器。
所有其他的線程在試圖進入已鎖定的監視器時將被掛起,直到鎖定了監視器的線程執行完synchronized(object)語句中的代碼塊,即監視器被解鎖
為止,另外的線程才可以進入并鎖定監視器。
一個剛鎖定了監視器的線程在監視器被解鎖后可以再次進入并鎖定同一監視器
,好比籃球運動員的籃球出手后可以再次去搶回來一樣。另外當在同步塊中遇到break語句或扔出異常時,線程也會釋放該鎖旗標
。
其實,程序并不能控制CPU的切換,程序是不可能抱著CPU的大腿不讓他走的。當CPU進入了一段同步代碼塊中執行,CPU是可以切換到其他線程的,只是在準備執行其他線程的代碼時,發現其他線程處于阻塞狀態,CPU又會回到先前的線程上
。大家也看到同步處理后,程序的運行速度比原來沒有使用同步處理前更慢了,因為系統要不停地對同步監視器進行檢查,需要更多的開銷。同步是以犧牲程序的性能為代價的,如果我們能夠確定程序沒有安全性的問題,就沒必要使用同步控制
。
小結
1、synchronized (obj)傳入的實參必須是同一個對象,可以是一個字符串對象,可以傳入this用當前類來表示這個對象等,但是不可以在run里面new出對象,也就是要確保對象的唯一性。
2、synchronized (obj)傳入的實參對象就是一個鎖旗幟,(鎖旗幟為了方便理解也可以認為是一個標志位)
3、一個線程對象嘗試獲取鎖旗幟,如果能獲取那么就順利執行邏輯,如果獲取不到就會被放進 等待線程池 ,被掛起,處于阻塞狀態
4、同步代碼塊中,一個鎖旗幟只能由于一個線程對象所持有。
5、一個剛鎖定了監視器的線程在監視器被解鎖后可以再次進入并鎖定同一監視器,不是說解鎖之后就一定是其他線程獲得鎖旗幟。
6、當CPU進入了一段同步代碼塊中執行,CPU是可以切換到其他線程的,只是在準備執行其他線程的代碼時,發現其他線程處于阻塞狀態,CPU又會回到先前的線程上當CPU進入了一段同步代碼塊中執行,CPU是可以切換到其他線程的,只是在準備執行其他線程的代碼時,發現其他線程處于阻塞狀態,CPU又會回到先前的線程上
7、多線程同步會更多地消耗cpu的性能,如果可以確定沒有線程安全問題,多線程沒必要進行同步。
.
.
.
五.2.2、 同步函數/同步方法
格式
在方法上加上synchronized修飾符即可。(一般寫在run方法之外!)
synchronized 返回值類型 方法名(參數列表)
{
/**.....
TODO SOMETHING
*/
}
同步方法的同步監聽器其實的是 this
(因為static不能調用this,因此如果是靜態同步方法,默認監聽器是當前方法所在類的.class對象)
同步方法 示例
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號窗口").start();
new Thread(sell, "2號窗口").start();
new Thread(sell, "3號窗口").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-- + "張票!");
}
}
}
可見,在函數定義前使用synchronized 關鍵字也能夠很好實現線程間的同步。
當有一個線程進入了synchronized 修飾的方法(獲得監視器),其他線程就不能進入同一個對象的所有使用了synchronized 修飾的方法, 直到第一個線程執行完它所進入的synchronized 修飾的方法為止(離開監視器)。
.
.
實現代碼塊與函數之間的可以實現同步
實現代碼塊與函數之間的可以實現同步,前提是兩者使用的必須是同一個監視器。
五.2.3、 同步鎖 ReentrantLock
Lock是java.util.concurrent.locks包下的接口,ReentrantLock類是唯一一個Lock接口的實現類,它的意思是可重入鎖,我們這里先演示簡單實用,后面會有專門的章節談論Lock。
Java類庫中為我們提供了能夠給臨界區“上鎖”的ReentrantLock類,它實現了Lock接口。
關于“臨界區”,后文我們會涉及。
示例代碼
import java.util.concurrent.locks.ReentrantLock;
public class TestClass {
public static void main(String[] args) {
SellRunnable sell = new SellRunnable();
new Thread(sell, "1號窗口").start();
new Thread(sell, "2號窗口").start();
new Thread(sell, "3號窗口").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(); // 解鎖
}
}
}
用法和同步方法類似,注意上鎖和手動解鎖。
.
.
輸出依然沒有安全問題的產生。
六、競爭條件與臨界區
在同一程序中運行多個線程本身不會導致問題,問題在于多個線程訪問了相同的資源。如,同一內存區(變量,數組,或對象)、系統(數據庫,web services 等)或文件。實際上,這些問題只有在一或多個線程向這些資源做了寫操作時才有可能發生,只要資源沒有發生變化,多個線程讀取相同的資源就是安全的。
多線程同時執行下面的代碼可能會出錯:
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
想象下線程 A 和 B 同時執行同一個 Counter 對象的 add()方法,我們無法知道操作系統何時會在兩個線程之間切換。JVM 并不是將這段代碼視為單條指令來執行的,而是按照下面的順序:
從內存獲取 this.count 的值放到寄存器
將寄存器中的值增加 value
將寄存器中的值寫回內存
觀察線程 A 和 B 交錯執行會發生什么:
this.count = 0;
A: 讀取 this.count 到一個寄存器 (0)
B: 讀取 this.count 到一個寄存器 (0)
B: 將寄存器的值加 2
B: 回寫寄存器值(2)到內存. this.count 現在等于 2
A: 將寄存器的值加 3
A: 回寫寄存器值(3)到內存. this.count 現在等于 3
兩個線程分別加了 2 和 3 到 count 變量上,兩個線程執行結束后 count 變量的值應該等于 5。然而由于兩個線程是交叉執行的,兩個線程從內存中讀出的初始值都是 0。然后各自加了 2 和 3,并分別寫回內存。最終的值并不是期望的 5,而是最后寫回內存的那個線程的值,上面例子中最后寫回內存的是線程 A,但實際中也可能是線程 B。如果沒有采用合適的同步機制,線程間的交叉執行情況就無法預料。
競爭條件(race condition)
當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。
臨界區(critical area)
導致競態條件發生的代碼區稱作臨界區。
上例中 add()方法就是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就可以避免競態條件。
七、Lock與synchronized
七.1、Lock與synchronized
Java中可以使用 Lock 和 synchronized 都可以實現對某個共享資源的同步,同時也可以實現對某些過程的原子性操作。Lock內部也使用了synchronized。
通常用法
synchronized:
在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號中表示需要鎖的對象。Lock:
需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個線程中必須要使用一個ReentrantLock類做為對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。
性能上的一點事
在 JDK1.5 中,synchronized 是性能低效的。因為這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的并發性帶來了很大的壓力。相比之下使用Java 提供的 Lock 對象,性能更高一些。Brian Goetz 對這兩種鎖在 JDK1.5、單核處理器及雙 Xeon 處理器環境下做了一組吞吐量對比的實驗,發現多線程環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock 則能基本保持在同一個比較穩定的水平上。但與其說 ReetrantLock 性能好,倒不如說 synchronized 還有非常大的優化余地。
于是到了 JDK1.6,發生了變化,對 synchronize 加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在 JDK1.6 上 synchronize 的性能并不比 Lock 差。官方也表示,他們也更支持 synchronize,在未來的版本中還有優化余地,所以還是提倡在 synchronized 能實現需求的情況下,優先考慮使用 synchronized 來進行同步。
Lock可以使用Condition進行線程之間的調度,Synchronized則使用Object對象本身的notify, wait, notityAll調度機制,這兩種調度機制有什么異同呢?
Condition是Java5以后出現的機制,它有更好的靈活性,而且在一個對象里面可以有多個Condition(即對象監視器),線程可以注冊在不同的Condition,從而可以有選擇性的調度線程,更加靈活。
Synchronized就相當于整個對象只有一個單一的Condition(即該對象本身)所有的線程都注冊在它身上,線程調度的時候之后調度所有得注冊線程,沒有選擇權,會出現相當大的問題 。
七.2、Lock
七.2.1 Lock接口和方法
Java 5 中引入了新的鎖機制——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方法:用來獲取鎖,在鎖被占用時它會一直阻塞,并且這個方法不能被中斷;
- lockInterruptibly方法:在獲取不到鎖時也會阻塞,它與lock方法的區別在于阻塞在該方法時可以被中斷;
- tryLock方法:也是用來獲取鎖的,它的無參版本在獲取不到鎖時會立刻返回false,它的計時等待版本會在等待指定時間還獲取不到鎖時返回false,計時等待的tryLock在阻塞期間也能夠被中斷。使用tryLock方法的典型代碼如下:
if (myLock.tryLock()) {
try {
…
} finally {
myLock.unlock();
}
} else {
//做其他的工作
}
unlock方法:用來釋放鎖;
newCondition方法:用來獲取當前鎖對象相關的條件對象。
七.2.2 關于鎖的一些概念
可重入鎖
可重入鎖的概念是自己可以再次獲取自己的內部鎖。舉個例子,比如一條線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的(如果不可重入的鎖的話,此刻會造成死鎖)。說的更高深一點可重入鎖是一種遞歸無阻塞的同步機制。
讀寫鎖
讀寫鎖拆成讀鎖和寫鎖來理解。讀鎖可以共享,多個線程可以同時擁有讀鎖,但是寫鎖卻只能只有一個線程擁有,而且獲取寫鎖的時候其他線程都已經釋放了讀鎖,而且該線程獲取寫鎖之后,其他線程不能再獲取讀鎖。
簡單的說就是寫鎖是排他鎖,讀鎖是共享鎖。
公平鎖和非公平鎖
獲取鎖涉及到的兩個概念即 公平和非公平
公平鎖
公平表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO順序。非公平鎖
非公平就是一種獲取鎖的搶占機制,和公平相對就是先來不一定先得,這個方式可能造成某些線程饑餓(一直拿不到鎖)。
關于鎖頭的其他詳細分類可以查看 Java鎖的種類以及辨析
Lock 接口有 3 個實現它的類
- ReentrantLock 重入鎖
- ReetrantReadWriteLock.ReadLock 讀鎖
- ReetrantReadWriteLock.WriteLock 寫鎖
Lock 必須被顯式地創建、鎖定和釋放,為了可以使用更多的功能,一般用 ReentrantLock 為其實例化。為了保證鎖最終一定會被釋放(可能會有異常發生),要把互斥區放在 try 語句塊內,并在 finally 語句塊中釋放鎖,尤其當有 return 語句時,return 語句必須放在 try 字句中,以確保 unlock()不會過早發生,從而將數據暴露給第二個任務。因此,采用 lock 加鎖和釋放鎖的一般形式如下:
Lock lock = new ReentrantLock();//默認使用非公平鎖,如果要使用公平鎖,需要傳入參數true
........
lock.lock();
try {
//更新對象的狀態
//捕獲異常,必要時恢復到原來的不變約束
//如果有return語句,放在這里
finally {
lock.unlock(); //鎖必須在finally塊中釋放
}
關于線程的同步的先到這里。
八、線程間的通信
線程通信的目標是使線程間能夠互相發送信號。另一方面,線程通信使線程能夠等待其他線程的信號。
我們用一個經典的生產者和消費者的代碼來演示這個線程間的通信。
需求:生產者產出一個產品,消費者就消費一個產品
需要控制好通信和線程安全的問題,不能出現
1、產品還沒生產就被消費了
2、一個產品被多次重復消費
3、不能生產了一批就開始消費(要求生產一個就馬上消費一個)
涉及到進程間通訊的幾個方法
寫代碼之前,我們先來看一下幾個方法
wait()
讓當前線程放棄監視器進入等待,直到其他線程調用同一個監視器并調用notify()或notifyAll()為止。notify()
喚醒在同一對象監聽器中調用wait方法的第一個線程。notifyAll()
喚醒在同一對象監聽器中調用wait方法的所有線程。
wait()、notify()、notifyAll()的調用細節
wait()、notify()、notifyAll(),這三個方法屬于Object 不屬于 Thread
這三個方法必須由同步監視對象來調用,兩種情況:
- 1.synchronized修飾的方法,因為該類的默認實例(this)就是同步監視器,所以可以在同步方法中調用這三個方法;
- 2.synchronized修飾的同步代碼塊,同步監視器是括號里的對象,所以必須使用該對象調用這三個方法;
可要是我們使用的是Lock對象來保證同步的,系統中不存在隱式的同步監視器對象,那么就不能使用者三個方法了,那該咋辦呢?
此時,Lock代替了同步方法或同步代碼塊,Condition代替了同步監視器的功能;
Condition對象通過Lock對象的newCondition()方法創建;
里面方法包括:
- await(): 等價于同步監聽器的wait()方法;
- signal(): 等價于同步監聽器的notify()方法;
- signalAll(): 等價于同步監聽器的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;//內存區為空!
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) {
// 生產的產品不為空,還沒被消費,就不生產,放棄監視器進入等待
while(isimpty.equals(Boolean.FALSE)){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;//為空的話生產者創造!
this.price = sex;
isimpty = Boolean.FALSE;//創造結束后修改屬性!
System.out.println("生產 +++ 產品:"+getName()+ ", "+"價格:"+getPrice());
this.notifyAll();
}
}
public void getGoods(){
synchronized (this) {
// 生產的產品為空,沒有產品可以被消費,就不消費,放棄監視器進入等待
while(isimpty.equals(Boolean.TRUE)){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費 --- 產品:"+getName()+ ", "+"價格:"+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("產品編號為:"+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();
}
}
}
如上代碼,
當我們生產時候,
如果發現已經有產品存在沒被消費,那么這個生產的線程就wait,不接著往下執行生產的代碼了;
如果產品為空,就這順利執行生產的代碼,并且調用 this.notifyAll(); 通知消費者線程可以來消費了當我們消費的時候
如果沒有產品可以被消費,那么消費者線程就wait,不接著往下執行消費的代碼了;
如果產品不為空,就順利執行消費的代碼,消費完成之后調用 this.notifyAll();喚醒之前因為生產完被職位wait的生產者線程。
.
.
輸出
生產 +++ 產品:產品編號為:0, 價格:10.00
消費 --- 產品:產品編號為:0, 價格:10.00
生產 +++ 產品:產品編號為:1, 價格:11.00
消費 --- 產品:產品編號為:1, 價格:11.00
生產 +++ 產品:產品編號為:2, 價格:12.00
消費 --- 產品:產品編號為:2, 價格:12.00
生產 +++ 產品:產品編號為:3, 價格:13.00
消費 --- 產品:產品編號為:3, 價格:13.00
生產 +++ 產品:產品編號為:4, 價格:14.00
消費 --- 產品:產品編號為:4, 價格:14.00
生產 +++ 產品:產品編號為:5, 價格:15.00
消費 --- 產品:產品編號為:5, 價格:15.00
生產 +++ 產品:產品編號為:6, 價格:16.00
消費 --- 產品:產品編號為:6, 價格:16.00
生產 +++ 產品:產品編號為:7, 價格:17.00
消費 --- 產品:產品編號為:7, 價格:17.00
生產 +++ 產品:產品編號為:8, 價格:18.00
消費 --- 產品:產品編號為:8, 價格:18.00
生產 +++ 產品:產品編號為:9, 價格:19.00
消費 --- 產品:產品編號為:9, 價格:19.00
生產 +++ 產品:產品編號為:10, 價格:20.00
消費 --- 產品:產品編號為:10, 價格:20.00
生產 +++ 產品:產品編號為:11, 價格:21.00
消費 --- 產品:產品編號為:11, 價格:21.00
利用 代碼展示線程狀態