Java多執行緒學習(基礎篇)
1. java對執行緒的支援
java對執行緒的支援主要體現在Thread類以及Runable介面上,他們都位於java.lang包下,無論是Thread類還是Runable介面,它們都有public void run()方法,這個run方法為我們提供了執行緒實際工作時的程式碼,換句話說,我們的邏輯程式碼就可以寫在run方法體中。
那麼什麼時候該用Thread,什麼時候該用Runable呢
繼承Thread實現的模式是 定義多個執行緒,各自完成各自的任務. 實現Runnable實現的模式是 定義多個執行緒,實現一個任務.其實在實現一個任務用多個執行緒來做也可以用繼承Thread類來實現只是比較麻煩,一般我們用實現Runnable介面來實現,簡潔明瞭。大多數情況下,如果只想重寫 run() 方法,而不重寫其他 Thread 方法,那麼應使用 Runnable 介面。
2. 執行緒的常用方法
3. 如何正確地停止執行緒
不要呼叫執行緒的stop()方法來停止執行緒,呼叫stop方法停止執行緒會有一種戛然而止的感覺,你並不知道它是執行到哪一步停止的,也不知道還有什麼邏輯沒執行完。應該設定一個結束標誌來結束執行緒。也不要呼叫interrupt()方法來停止執行緒,因為interrupt()方法的初衷並不是用於停止執行緒的,而是用於中斷執行緒。
4 interrupt
① interrupt的作用就是中斷執行緒,呼叫interput()方法會設定執行緒的中斷狀態。
② 然後呼叫執行緒的interrupted()【這是一個靜態方法】方法或isInterrupted()【這是一個例項方法】方法將會返回一個boolean值(執行緒的中斷狀態)。
③ 當一個執行緒,呼叫join,sleep方法而被阻塞時,會使執行緒的中斷狀態被清除,然後後面呼叫interrupted()或isInterrupted()方法都不能得到一個正確的結果,並且當前執行緒會收到一個InterruptedException異常,這也就是為什麼我們需要在程式碼sleep方法和join方法中進行try catch的原因。
package com.glassbox.thread.demo;
/**
* @auther xiehuaxin
* @create 2018-08-08 10:16
* @todo
*/
public class InterruptTest extends Thread {
public static void main(String[] args) {
System.out.println("begin>>>>>>>>>>>>>>>");
Thread interruptTest = new InterruptTest();
interruptTest.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("interrupt Thread>>>>>>>");
interruptTest.interrupt();
System.out.println("stop>>>>>>>>>>>>>>>>");
}
@Override
public void run() {
while (true) {
System.out.println("Thread is running...");
/**
* 這裡用這種方式代替sleep是因為呼叫sleep的時候會改變當前執行緒的“中斷狀態”,從而丟擲一個InterreptException異常
*/
long time = System.currentTimeMillis();
while (System.currentTimeMillis() - time < 1000) {
}
}
}
}
輸出: 可以看到,當呼叫interrupt方法後,執行緒仍在輸出
begin>>>>>>>>>>>>>>>
Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt Thread>>>>>>>
stop>>>>>>>>>>>>>>>>
Thread is running...
Thread is running...
Thread is running...
Thread is running...
可以把isInterrupted()的返回結果當做結束標誌
@Override
public void run() {
while (!this.isInterrupted()) {
System.out.println("Thread is running...");
/**
* 這裡用這種方式代替sleep是因為呼叫sleep的時候會改變當前執行緒的“中斷狀態”,從而丟擲一個InterreptException異常
*/
long time = System.currentTimeMillis();
while (System.currentTimeMillis() - time < 1000) {
}
}
}
5. 執行緒中的能量不守恆
“爭用條件”:當多個執行緒同時共享訪問同一資料(記憶體區域)時,每個執行緒都嘗試操作該資料,從而導致資料被破壞(corrupted),這種現象稱為爭用條件。
package com.glassbox.thread.demo2;
/**
* @auther xiehuaxin
* @create 2018-08-08 10:52
* @todo
*/
/**
* 宇宙的能量系統
* 遵循能量守恆定律
* 能量不會憑空產生也不會憑空消失,只會從一處轉移到另一處
*/
public class EnergySystem {
//能量盒子,能儲存能量的地方
private final double[] energyBox;
/**
*
* @param n 能量盒子的數量
* @param initalEnergy 每個能量盒子初始含有的能量值
*/
public EnergySystem(int n, double initalEnergy) {
this.energyBox = new double[n];
for (int i = 0; i < energyBox.length; i++) {
energyBox[i] = initalEnergy;
}
}
/**
* 能量的轉移,從一個盒子轉移到另外一個盒子
* @param from 能量源
* @param to 能量終點
* @param amount 要轉移的能量值
*/
public void transfer(int from, int to, double amount) {
if(energyBox[from] < amount) {
return;
}
System.out.println(Thread.currentThread().getName());
energyBox[from] -= amount;
System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
energyBox[to] += amount;
System.out.printf("能量總和:%10.2f%n", getTotalEnergies());
}
/**
* 獲取能量世界的能量總和
* @return
*/
private double getTotalEnergies() {
double sum = 0;
for (double amount : energyBox) {
sum += amount;
}
return sum;
}
/**
* 返回能量盒子的長度
* @return
*/
public int getBoxAmount() {
return energyBox.length;
}
}
package com.glassbox.thread.demo2;
/**
* @auther xiehuaxin
* @create 2018-08-08 11:48
* @todo
*/
public class EnergyTransferTask implements Runnable {
//共享的能量世界
private EnergySystem energySystem;
//能量轉移的能源盒子下班
private int fromBoxIndex;
//單次能量轉移最大單元
private double maxAmount;
//最大休眠時間(毫秒)
private int DELAY = 10;
public EnergyTransferTask(EnergySystem energySystem, int fromBoxIndex, double max) {
this.energySystem = energySystem;
this.fromBoxIndex = fromBoxIndex;
this.maxAmount = max;
}
@Override
public void run() {
try {
while (true) {
int toBox = (int) (energySystem.getBoxAmount() * Math.random());
double amount = maxAmount * Math.random();
energySystem.transfer(fromBoxIndex, toBox, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.glassbox.thread.demo2;
/**
* @auther xiehuaxin
* @create 2018-08-08 11:59
* @todo
*/
public class EnergySystemTest {
//要構建的能量世界的盒子的數量
public static final int BOX_AMOUNT = 100;
//每個盒子的初始能量
public static final double INITIAL_ENERGY = 1000;
public static void main(String[] args) {
EnergySystem energySystem = new EnergySystem(BOX_AMOUNT,INITIAL_ENERGY);
for (int i = 0; i < BOX_AMOUNT; i++) {
EnergyTransferTask task = new EnergyTransferTask(energySystem,i,INITIAL_ENERGY);
Thread thread = new Thread(task,"TransferThread:" + i);
thread.start();
}
}
}
輸出
從38轉移 174.83單位能量到0能量總和: 100000.00
TransferThread:38
從38轉移 27.28單位能量到9能量總和: 100000.00
TransferThread:52
TransferThread:98
從98轉移 584.27單位能量到49能量總和: 99390.63
TransferThread:98
從98轉移 126.47單位能量到20能量總和: 99390.63
TransferThread:98
從98轉移 264.87單位能量到98能量總和: 99390.63
TransferThread:74
從74轉移 873.16單位能量到42能量總和: 99390.63
TransferThread:50
從50轉移 439.71單位能量到47能量總和: 99390.63
TransferThread:70
從70轉移 29.80單位能量到34能量總和: 99390.63
能量總和: 100000.00
TransferThread:75
從75轉移 377.40單位能量到78能量總和: 99390.63
TransferThread:95
從95轉移 74.24單位能量到76能量總和: 99390.63
能量總和: 100000.00
TransferThread:71
從71轉移 285.92單位能量到10能量總和: 99390.63
TransferThread:83
從83轉移 778.90單位能量到66能量總和: 99390.63
TransferThread:46
從46轉移 515.10單位能量到63能量總和: 99390.63
從52轉移 609.37單位能量到40能量總和: 100000.00
6. 那麼如何保持能量守恆呢
互斥與同步
互斥:字面意思是互相排斥,實際含義是,同一時間只能有一個執行緒對臨界區或關鍵資料進行操作(通過鎖)。
同步:執行緒間的一種通訊機制。一個執行緒它可以做一件事情,然後它可以用某種方式告訴別的執行緒它已經做完了(notify,wait,notifyAll)。
package com.glassbox.thread.demo2;
/**
* @auther xiehuaxin
* @create 2018-08-08 10:52
* @todo
*/
/**
* 宇宙的能量系統
* 遵循能量守恆定律
* 能量不會憑空產生也不會憑空消失,只會從一處轉移到另一處
*/
public class EnergySystem {
//能量盒子,能儲存能量的地方
private final double[] energyBox;
//新增一個鎖的物件
private final Object lockObj = new Object();
/**
*
* @param n 能量盒子的數量
* @param initalEnergy 每個能量盒子初始含有的能量值
*/
public EnergySystem(int n, double initalEnergy) {
this.energyBox = new double[n];
for (int i = 0; i < energyBox.length; i++) {
energyBox[i] = initalEnergy;
}
}
/**
* 能量的轉移,從一個盒子轉移到另外一個盒子
* @param from 能量源
* @param to 能量終點
* @param amount 要轉移的能量值
*/
public void transfer(int from, int to, double amount) {
/**
* 通過對lockObj物件加鎖實現互斥
*/
synchronized (lockObj) {
//這裡當能量不足的時候就退出,但是在退出之後,這條執行緒仍然有機會去獲取cpu資源,從而再次要求加鎖,但是加鎖操作時有開銷的,這樣會降低系統的效能,所以當條件不滿足的時候我們可以讓這條執行緒等待,從而降低這條執行緒獲取鎖的開銷
/*if(energyBox[from] < amount) {
return;
}*/
/**
* while迴圈,保證條件不滿足時任務都會被阻擋,而不是競爭cpu資源
*/
while (energyBox[from] < amount) {
try {
//當執行緒呼叫lockObj物件的wait()方法之後,當前的執行緒就會被置於lockObj物件的“等待集合wait set”中
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName());
energyBox[from] -= amount;
System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
energyBox[to] += amount;
System.out.printf("能量總和:%10.2f%n", getTotalEnergies());
//喚醒所有在lockObj物件上等待的執行緒
lockObj.notifyAll();
}
}
/**
* 獲取能量世界的能量總和
* @return
*/
private double getTotalEnergies() {
double sum = 0;
for (double amount : energyBox) {
sum += amount;
}
return sum;
}
/**
* 返回能量盒子的長度
* @return
*/
public int getBoxAmount() {
return energyBox.length;
}
}
輸出
從68轉移 765.10單位能量到80能量總和: 100000.00
TransferThread:69
從69轉移 564.22單位能量到33能量總和: 100000.00
TransferThread:64
從64轉移 941.32單位能量到96能量總和: 100000.00
TransferThread:96
從96轉移 511.90單位能量到74能量總和: 100000.00
TransferThread:4
從4轉移 424.99單位能量到70能量總和: 100000.00
TransferThread:7
從7轉移 429.73單位能量到33能量總和: 100000.00
7. 如何擴充套件java併發程式設計的知識
① 瞭解Java Memory Model
JMM描述了java執行緒如何通過記憶體進行互動的(通過這部分學習可以瞭解什麼是happens-before原則)
happens-before
synchronized,volatile&final(java是如何通過這幾個關鍵字來實現happens-before原則的)
② Locks物件和Condition物件(通過這兩個物件可以瞭解如何對程式進行加鎖以及同步的通訊)
java鎖機制和等待條件的高層實現
java.util.concurrent.locks
③ 執行緒安全性
原子性、可見性
java.util.concurrent.atomic(如何通過這個包來避免原子性程式設計的問題)
synchronized & volatile(當一個原子操作由多個語句構成時,又是如何通過synchronized實現原子性操作的)
DeadLocks
④ 多執行緒常用的互動模型(在java併發實現當中,有哪些類是實現了這些模型,可以供我們直接呼叫的)
Producer-Consumer模型
Read-Write Lock模型
Future模型
Woker Thread模型
⑤ Java5引入的併發程式設計工具
java.util.concurrent
執行緒池ExecutorService
Callable & Future
BlockingQueue
最後推薦兩邊書
練習鞏固
題目:建立兩個執行緒,一個執行緒輸出1、3、5、…、99,另一個執行緒輸出2、4、6、…、100。最後的效果是兩個執行緒相互交替輸出,達到輸出1、2、3、4、…、99、100這樣順序輸出的效果。
以下是我自己的實現:
package com.glassbox.thread.test;
/**
* @auther xiehuaxin
* @create 2018-08-08 16:08
* @todo
*/
public class PrintNum implements Runnable {
int i = 1;
@Override
public void run() {
while (true) {
//如果不是使用實現Runnable這種方式建立執行緒,這裡不要用this(自己建立一個鎖物件)
synchronized (this) {
//這裡先呼叫鎖物件的notify方法再呼叫wait方法是巧妙之處,大家細細體會,如果還不明白的話可以把notify方法放到wait方法後,然後執行輸出結果就一目瞭然了
notify();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
package com.glassbox.thread.test;
import java.util.concurrent.*;
/**
* @auther xiehuaxin
* @create 2018-08-08 15:41
* @todo
*/
public class ThreadExercise {
/**
* 任務佇列
*/
private static ArrayBlockingQueue workQueue = new ArrayBlockingQueue(10);
public static void main(String[] args) {
/*
執行緒池最大數量
*/
int maximumPoolSizeSize = 100;
/*
執行緒活動保持時間
*/
long keepAliveTime = 1;
/*
執行緒池的基本大小
*/
int corePoolSize = 10;
//建議手動建立執行緒池並且以執行緒池的形式建立執行緒
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSizeSize, keepAliveTime, TimeUnit.SECONDS, workQueue, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("hahaThread");
return thread;
}
});
PrintNum num = new PrintNum();
executor.execute(num);
executor.execute(num);
}
}