java多執行緒程式設計詳細入門教程
##1、概念
執行緒是jvm排程的最小單元,也叫做輕量級程序,程序是由執行緒組成,執行緒擁有私有的程式技術器以及棧,並且能夠訪問堆中的共享資源。這裡提出一個問題,為什麼要用多執行緒?有一下幾點,首先,隨著cpu核心數的增加,計算機硬體的平行計算能力得到提升,而同一個時刻一個執行緒只能執行在一個cpu上,那麼計算機的資源被浪費了,所以需要使用多執行緒。其次,也是為了提高系統的響應速度,如果系統只有一個執行緒可以執行,那麼當不同使用者有不同的請求時,由於上一個請求沒處理完,那麼其他的使用者必定需要在一個佇列中等待,大大降低響應速度,所以需要多執行緒。這裡繼續介紹多執行緒的幾種狀態:
這裡可以看到多執行緒有六種狀態,分別是就緒態,執行態,死亡態,阻塞態,等待態,和超時等待態,各種狀態之間的切換如上圖所示。這裡的狀態切換是通過synchronized鎖下的方法實現,對應的Lock鎖下的方法同樣可以實現這些切換。
##2、執行緒的建立
執行緒的建立有兩種方式,第一種是繼承Thread類,第二種是實現Runnable介面。第一個程式碼:
class MyThread extends Thread{ int j=20; public void run(){ for (int i = 0; i < 20; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(this.getName()+",i="+j--); } } }
然後main函式中建立:
class MyThread extends Thread{ int j=20; public void run(){ for (int i = 0; i < 20; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(this.getName()+",i="+j--); } } }
第二種方法:
class MyRunnable implements Runnable{
int j=20;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+",j="+this.j--);
}
}
}
main函式中:
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
這就是兩種建立執行緒的方法,在這兩種方法中第二種方法時一般情況下的用法,因為繼承只能繼承一個類,但是可以實現多個介面,這樣拓展性更好。
##3、執行緒安全測試
執行緒安全是多執行緒程式設計中經常需要考慮的一個問題,執行緒安全是指多執行緒環境下多個執行緒可能會同時對同一段程式碼或者共享變數進行執行,如果每次執行的結果和單執行緒下的結果是一致的,那麼就是執行緒安全的,如果每次執行的結果不一致,那麼就是執行緒不安全的。這裡對執行緒安全做一個測試:
class MyRunnable implements Runnable{
static int j=10000;
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
System.out.println(j--);
}
}
}
main函式中:
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
可以看到,這裡同時兩個執行緒同時對共享變數j進行訪問,並且減1,但最後的輸出結果卻是:
48
47
46
並且多次執行程式的結果還不一致,這就是執行緒不安全的情況,通過加鎖可以保證執行緒安全。
##4、鎖
java中有兩種鎖,一種是重量級鎖synchronized,jdk1.6經過鎖優化加入了偏向鎖和輕量級鎖,一種是JUC併發包下的Lock鎖,synchronized鎖也稱物件鎖,每個物件都有一個物件鎖。這裡通過加鎖的方式實現執行緒安全:
程式碼:
class MyRunnable implements Runnable{
static int j=10000;
@Override
public synchronized void run() {
for (int i = 0; i < 5000; i++) {
System.out.println(j--);
}
}
}
main中建立兩個執行緒,測試多次的結果都是:
3
2
1
說明實現的執行緒安全,因為當加鎖過後,每次只能有一個執行緒訪問被加鎖的程式碼,這樣就不會出現執行緒安全了。
##5、sleep
sleep是讓當前執行緒睡眠,睡眠一段時間後重新獲取到cpu的使用權。
程式碼如下:
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
這裡表示執行緒會睡眠100ms後再次到就緒狀態中,這裡為什麼sleep是Thread的類方法而不是執行緒的方法,因為,能呼叫sleep的執行緒肯定是執行著的,而其他執行緒也是未執行的,所以呼叫其他執行緒的sleep是沒有意義的。
##6、wait、notify
wait表示當前執行緒釋放掉鎖進入等待狀態,所以呼叫wait和notify的前提是已經獲取到物件的鎖,如果沒有獲取到鎖就使用wait那麼會出異常資訊。而進入等待狀態的執行緒需要通過notify或者通過中斷來喚醒進入到阻塞狀態來等待鎖的獲取。這裡對這種情況進行測試,使用notify喚醒一個等待狀態的執行緒:
class MyThreadd extends Thread{
public MyThreadd(String name) {
// TODO Auto-generated constructor stub
super(name);
}
public void run(){
synchronized (this) {
System.out.println(Thread.currentThread().getName()+" notify a thread");
notify();
}
while(true);
}
main中:
MyThreadd t1 = new MyThreadd("t1");
synchronized (t1) {
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start();
System.out.println(Thread.currentThread().getName()+" wait");
t1.wait();
System.out.println(Thread.currentThread().getName()+" waked up");
}
這裡可以看到,在main函式中,主執行緒將建立一個執行緒t1然後進入t1的鎖的同步塊中啟動執行緒t1,然後呼叫wait進入等待狀態,這個時候執行緒t1也進入到同步塊中,呼叫notify後釋放掉鎖,可以看到主執行緒後續的東西繼續被輸出。當有多個執行緒呼叫了wait之後如果採用notify只會隨機的喚醒其中的一個執行緒進入阻塞態,而採用notifyall會將所有的執行緒給喚醒。線上程執行結束後會呼叫notifyall將所有等待狀態的執行緒喚醒。
##7、join
join的作用是讓父執行緒等待子執行緒執行結束後在執行,通過檢視原始碼可以知道:
其實也是呼叫了先獲取到子執行緒的鎖然後呼叫wait方法來實現的,因為當子執行緒執行結束後會呼叫notifyall所以主執行緒會被喚醒並且再次獲取到子執行緒的鎖繼續執行。
class MyRuu extends Thread{
public MyRuu(String name) {
super(name);
}
public void run() {
System.out.println(Thread.currentThread().getName());
//while(true);
}
}
main函式中:
MyRuu myRuu = new MyRuu("t1");
System.out.println(Thread.currentThread().getName()+" start t1");
myRuu.start();
System.out.println(Thread.currentThread().getName() +" join");
myRuu.join();
System.out.println(Thread.currentThread().getName() +" waked up");
執行結果:
main start t1
main join
t1
main waked up
可以看到,當主執行緒呼叫join後子執行緒開始執行,等子執行緒執行結束後主執行緒被喚醒。
##8、yeild
yeild的作用是執行緒讓步,當前線呼叫yeild方法後執行緒從執行態進入到就緒態重新進入到CPU資源的競爭中。這裡進行測試:
class MyRun extends Thread{
Object obj;
public MyRun(String name,Object obj) {
// TODO Auto-generated constructor stub
super(name);
this.obj = obj;
}
public void run(){
// synchronized (obj) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+ " i="+i);
if(i%2 == 0)
Thread.yield();
}
// }
}
}
main函式中:
Object obj = new Object();
// TODO Auto-generated method stub
MyRun t1 = new MyRun("t1",obj);
MyRun t2 = new MyRun("t2",obj);
t1.start();
t2.start();
結果:
t1 i=0
t2 i=0
t1 i=1
t2 i=1
t2 i=2
t1 i=2
t2 i=3
t2 i=4
t1 i=3
t1 i=4
t2 i=5
t2 i=6
t1 i=5
t1 i=6
t2 i=7
t2 i=8
t1 i=7
t1 i=8
t2 i=9
t1 i=9
可以看到他們兩個基本上是交替執行,而不用yeild讓步的話大概率一個執行緒執行完成了另一個執行緒才會執行。
##9、priority
priority代表執行緒的優先順序,在JVM中優先順序高的執行緒不一定會先執行,只是先執行的概率會比低優先順序的執行緒大。
##10、中斷
對於一個正常執行的執行緒中,中斷基本是沒有作用的,只是作為一個標誌位來查詢。而執行緒的其他幾種狀態下如sleep、join、wait狀態下是可以被中斷,並且通過中斷來跳出當前狀態的。
class RunInt extends Thread{
public void run() {
while(!this.isInterrupted()){
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" i="+i);
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+" interrupted!");
break;
}
}
}
System.out.println(Thread.currentThread().getName()+" dead");
}
}
main中:
RunInt r1 = new RunInt();
r1.start();
Thread.yield();
synchronized (r1) {
System.out.println(Thread.currentThread().getName()+" intertupt r1");
r1.interrupt();
}
結果
main intertupt r1
Thread-0 i=0
Thread-0 i=1
Thread-0 i=2
Thread-0 i=3
Thread-0 i=4
Thread-0 i=5
Thread-0 i=6
Thread-0 i=7
Thread-0 i=8
Thread-0 i=9
Thread-0 interrupted!
Thread-0 dead
可以看到,當主執行緒啟動子執行緒後,子執行緒會進入到迴圈中並且進入到睡眠狀態,然後主執行緒通過呼叫中斷讓子執行緒喚醒並且推出迴圈後死亡。
##11、死鎖
死鎖指的是,兩個執行緒互相等待對方釋放資源導致卡死。例子:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (A) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (B) {
System.out.println("haha");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (B) {
synchronized (A) {
System.out.println("xixi");
}
}
}
});
t1.start();
t2.start();
可以看到t1執行緒獲得A的鎖然後睡眠,然後t2執行緒獲得B的鎖然後再等待A釋放鎖,而執行緒t1睡眠完成後在等待t2釋放B的鎖,導致程式卡死。
##12、生產者與消費者
生產者和消費者是多執行緒中一個很常見的應用場景,這裡首先用一個共享變數實現生產者和消費者,接著再使用阻塞佇列實現。首先實現第一種:
倉庫程式碼:
class Depot{
private int capacity;
private int size=0;
public Depot(int c) {
// TODO Auto-generated constructor stub
this.capacity = c;
}
public synchronized void product(int count) throws InterruptedException{
while(count>0){
if(size >= capacity)
wait();
//真實生產數量
int realcount = (capacity-size)>=count?count:(capacity-size);
System.out.print(Thread.currentThread().getName()+"--本次想要生產:"+count+",本次實際生產:"+realcount);
//下次生產數量
count = count - realcount;
//倉庫剩餘
size += realcount;
System.out.println(",下次想要生產:"+count+",倉庫真實容量:"+size);
notifyAll();
}
}
public synchronized void comsume(int count) throws InterruptedException {
while(count>0){
if(size <= 0)
wait();
//真實消費數量
int realcount = (size>=count)?count:size;
System.out.print(Thread.currentThread().getName()+"--本次想要消費:"+count+",本次真實消費:"+realcount);
//下次消費數量
count = count - realcount;
//倉庫剩餘
size -= realcount;
System.out.println("下次想要消費:"+count+",倉庫剩餘:"+size);
notify();
}
}
}
生產者程式碼:
class Producer {
Depot depot;
public Producer(Depot depot) {
// TODO Auto-generated constructor stub
this.depot = depot;
}
public void produce(final int count) {
new Thread(){
public void run() {
try {
depot.product(count);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();
}
}
消費者程式碼:
class Consumer{
Depot depot;
public Consumer(Depot depot) {
// TODO Auto-generated constructor stub
this.depot = depot;
}
public void consume(final int count) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
depot.comsume(count);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
}
main中:
Depot depot = new Depot(100);
Producer producer = new Producer(depot);
Consumer consumer = new Consumer(depot);
producer.produce(60);
producer.produce(50);
producer.produce(30);
consumer.consume(50);
consumer.consume(110);
producer.produce(40);
結果:
Thread-0--本次想要生產:60,本次實際生產:60,下次想要生產:0,倉庫真實容量:60
Thread-1--本次想要生產:50,本次實際生產:40,下次想要生產:10,倉庫真實容量:100
Thread-4--本次想要消費:110,本次真實消費:100下次想要消費:10,倉庫剩餘:0
Thread-1--本次想要生產:10,本次實際生產:10,下次想要生產:0,倉庫真實容量:10
Thread-4--本次想要消費:10,本次真實消費:10下次想要消費:0,倉庫剩餘:0
Thread-5--本次想要生產:40,本次實際生產:40,下次想要生產:0,倉庫真實容量:40
Thread-2--本次想要生產:30,本次實際生產:30,下次想要生產:0,倉庫真實容量:70
Thread-3--本次想要消費:50,本次真實消費:50下次想要消費:0,倉庫剩餘:20
可以看到實現了生產者消費者模型。
第二種利用阻塞佇列實現。直接利用阻塞隊列當做倉庫,生產者:
class Pro1{
private BlockingQueue<Integer> blockingQueue1;
public Pro1(BlockingQueue<Integer> blockingQueue) {
// TODO Auto-generated constructor stub
this.blockingQueue1 = blockingQueue;
}
public void produce(final int count) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < count; i++) {
try {
Thread.sleep(100);
blockingQueue1.put(100);
System.out.println("生產者,倉庫剩餘容量"+blockingQueue1.size());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
}
}
消費者:
class Con1{
private BlockingQueue<Integer> blockingQueue;
public Con1(BlockingQueue<Integer> blockingQueue) {
// TODO Auto-generated constructor stub
this.blockingQueue = blockingQueue;
}
public void consume(final int count) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < count; i++) {
try {
Thread.sleep(100);
blockingQueue.take();
System.out.println("消費者,本次倉庫剩餘:"+blockingQueue.size());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
}
}
main函式:
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(5);
Pro1 pro1 = new Pro1(blockingQueue);
Con1 con1 = new Con1(blockingQueue);
pro1.produce(10);
con1.consume(7);
結果:
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量0
生產者,倉庫剩餘容量1
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量1
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量0
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量1
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量1
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量1
消費者,本次倉庫剩餘:0
生產者,倉庫剩餘容量1
生產者,倉庫剩餘容量2
生產者,倉庫剩餘容量3
這裡阻塞佇列的作用是,當容量不足的消費者進入等待佇列,而當容量有剩餘的時候消費者被喚醒,當容量已滿的時候生產者進入等待佇列,當容量被消費後生產者被喚醒。