以前一聽到多線程操作就感到好膩害好膩害的,如果你現在也是這種情況或許這篇文章能夠幫助到你。
1、什麼是多線程?
先了解兩個概念
進程:正在運行的程序,是系統進行資源分配和調用的獨立單位,有自己的內存空間和系統資源。
線程:是進程中的單個順序控制流,是一條執行路徑,線程是應用程序中執行的基本單元。
某位大神的總結:進程就相當於工廠,線程就是工廠裏的流水線,線程不能獨立存在,必須存在於進程中。
多進程:系統中同時存在多個並行的進程,則稱爲多進程。可通過電腦任務管理器查看正在運行的進程,比如用電腦聊QQ的同時看電影,就是多進程的體現。
多線程:線程是進程中的單個順序控制流,是一條執行路徑,一個進程如果有多條執行路徑,則稱爲多線程。比如給某人聊QQ的同時還可以接收到其他人的消息。
2、多線程實現方式
1.繼承Thread類
Java提供了Thread類,讓我們對線程進行操作。
class MyThread extends Thread {
@Override
public void run() {
/* 多線程執行的邏輯 */
super.run();
}
}
//開啓線程
new MyThread().start();
2.實現Runable接口
class MyRunnable implements Runnable {
@Override
public void run() {
/* 多線程執行的邏輯 */
}
}
//將實現了Runnable接口的類,以參數的形式傳遞給Thread類
new Thread(new MyRunnable()).start();
3.實現Callable接口
這是一種有返回值的線程,但是必須通過線程池使用。
public class Test {
public static void main(String[] args) {
// 生成線程池對象
ExecutorService pool = Executors.newCachedThreadPool();
// 提交Callable線程
pool.submit(new MyCallable());
}
}
class MyCallable implements Callable<String> {
// 通過指定範型 可以通過線程方法返回該類型的數據 而其他兩種實現方式都沒有返回值
@Override
public String call() throws Exception {
/* 多線程執行的邏輯 */
return "hello";
}
}
3、線程調度
分時調度模型 :所有線程輪流使用CPU的使用權,平均分配每個線程佔用CPU的時間片。
搶佔式調度模型:搶佔CPU使用權,其中優先級高的線程搶到CPU執行權的概率會越大,存在很大隨機性。Java採用的是搶佔式調度模型。
查看搶佔式調度效果:
public class Test {
public static void main(String[] args) throws InterruptedException {
// 生成線程對象
MyThread thread = new MyThread();
MyThread thread2 = new MyThread();
// 啓動線程
thread.start();
thread2.start();
//第一次輸出 //第二次輸出
//Thread-0:0 //Thread-1:0
//Thread-1:0 //Thread-0:0
//Thread-1:1 //Thread-1:1
//Thread-1:2 //Thread-0:1
//Thread-0:1 //Thread-0:2
//Thread-0:2 //Thread-1:2
}
}
// 線程類
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i <3; i++) {
// 輸出線程名和i的值
System.out.println(this.getName() + ":" + i);
}
super.run();
}
}
通過輸出可以看出線程搶佔是隨機的無規律可言(不設置優先級的情況下)。
如何設置線程優先級呢?
//Thread提供了相關方法
// 設置線程的優先級
void setPriority(int newPriority)
// 返回線程的優先級
int getPriority()
// 生成線程對象
MyThread thread = new MyThread();
// 獲取默認線程優先級
int priority = thread.getPriority();
System.out.println(priority);// 5
// 設置線程優先級
thread.setPriority(6);
// 輸出最小線程優先級
System.out.println(thread.MIN_PRIORITY);// 1
// 輸出最大線程優先級
System.out.println(thread.MAX_PRIORITY);// 10
可以看出線程的優先級的是1-10
4、線程控制
Thread類提供了相關方法對線程進行控制。
1.線程休眠sleep
// 生成線程對象
MyThread thread = new MyThread();
//線程休眠3000毫秒
thread.sleep(3000);
//獲取當前線程對象並設置其休眠200毫秒2000納秒
Thread.currentThread().sleep(200, 2000);
2.線程加入join
public static void main(String[] args) throws InterruptedException {
// 生成線程對象
MyThread thread = new MyThread("線程一");
MyThread thread2 = new MyThread("線程二");
// 啓動線程
thread.start();
// 線程加入 當調用了線程加入了之後在該放後啓動的線程會等這個線程執行完 在搶佔CPU執行權
thread.join();
thread2.start();
//未添加前輸出
// 線程一:0
// 線程一:1
// 線程二:0
// 線程一:2
// 線程二:1
// 線程二:2
//添加後輸出
// 線程一:0
// 線程一:1
// 線程一:2
// 線程二:0
// 線程二:1
// 線程二:2
}
}
// 線程類
class MyThread extends Thread {
public MyThread(String name) {
// 設置線程名
this.setName(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
// 輸出線程名和i的值
System.out.println(this.getName() + ":" + i);
}
super.run();
}
}
3.線程禮讓yield
// 生成線程對象
MyThread thread = new MyThread("線程一");
MyThread thread2 = new MyThread("線程二");
//暫停當前正在執行的線程對象,並執行其他線程,讓線程執行更和諧
thread.yield();
// 啓動線程
thread.start();
thread2.start();
//輸出
// 線程一:0
// 線程二:0
// 線程一:1
// 線程二:1
// 線程二:2
// 線程一:2
4.線程守護setDaemon
// 生成線程對象
MyThread thread = new MyThread("線程一");
MyThread thread2 = new MyThread("線程二");
//將該線程標記爲守護線程或用戶線程 在啓動線程前調用
//設置thread爲守護線程 即當線程thread2執行完成後,如果thread未執行完則不再繼續執行
//因爲線程具有隨機性,所以存在thread在thread2執行完的情況
thread.setDaemon(true);
// 啓動線程
thread.start();
thread2.start();
5.線程中斷stop、interrupt
// 生成線程對象
MyThread thread = new MyThread("線程一");
MyThread thread2 = new MyThread("線程二");
// 啓動線程
thread.start();
thread2.start();
// 立即終止某個線程,沒有任何反饋,存在不安全性
//thread.stop();
// 中斷線程
thread2.interrupt();
//輸出:
//線程二線程終止!!!
//線程二------------
// 線程類
class MyThread extends Thread {
public MyThread(String name) {
// 設置線程名
this.setName(name);
}
@SuppressWarnings("static-access")
@Override
public void run() {
try {
//當前線程休眠3秒
this.sleep(3000);
} catch (InterruptedException e) {
System.out.println(this.getName()+"線程終止!!!");
}
System.out.println(this.getName()+"------------");
super.run();
}
}
stop()方法會立刻終止線程並沒有任何反饋,而interrupt()會有後續的輸出。
5、線程生命週期
圖示:
6、線程安全問題
1.線程安全問題
模擬賣票情況:兩個線程對同一數據進行操作
public class Test {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
new Thread(runnable, "賣票口一:").start();
new Thread(runnable, "賣票口二:").start();
}
}
// 線程類
class MyRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
// 多次執行
while (true) {
if (ticket > 0) {
try {
// 線程休眠模擬網絡延遲
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ ticket-- + "張票");
}
}
}
}
部分輸出情況:
1.出現同票情況?根據輸出可以推斷,當ticket爲40的時候,賣票窗口一線程搶到CPU執行權,並執行,但是執行run方法輸出語句的時候, ticket–只執行完賦值操作並未執行–操作的時候,CPU執行權被賣票窗口二搶到,輸出賣票窗口二:正在出售第40張票,然後賣票窗口一又搶到了CPU執行權輸出賣票窗口一:正在出售第40張票。
2.出現0票情況?根據輸出可以推斷,當ticket爲1的時候,賣票窗口一線程搶到CPU執行權,並執行,但是執行run方法未執行到輸出語句的時候,CPU執行權並執行完run方法輸出賣票窗口二:正在出售第1張票,然後賣票窗口一又搶到CPU執行權,此刻ticket已經爲0了, 所以輸出賣票窗口一:正在出售第0張票。
注:一般多線程環境、存在共享數據、多條語句操作共享數據的情況下會出現線程安全問題。那麼如何解決呢?就用到了線程同步。
2.2.線程同步
1.同步代碼塊
class MyRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
// 多次執行
while (true) {
// 同步代碼塊(添加任意對象)
synchronized (this) {
if (ticket > 0) {
try {
// 線程休眠模擬網絡延遲
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + ticket-- + "張票");
}
}
}
}
}
注:同步代碼塊格式synchronized(對象){同步代碼;};同步可以解決安全問題的根本原因就在那個對象上,該對象就是鎖,多個線程操作同一代碼塊,一定要保證那個鎖對象相同。
###2.同步方法
就是在方法上同步關鍵字。
class MyRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
// 多次執行
while (true) {
sellTicket();
}
}
/**
* 同步方法
*/
private synchronized void sellTicket() {
if (ticket > 0) {
try {
// 線程休眠模擬網絡延遲
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ ticket-- + "張票");
}
}
/**
* 同步靜態方法
*/
private static synchronized void sellTicket() {
if (ticket > 0) {
try {
// 線程休眠模擬網絡延遲
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ ticket-- + "張票");
}
}
注:同步方法的鎖對象其實是當前對象this,同步靜態方法的鎖對象是當前類的字節碼文件。
3.Lock鎖的使用
JDK5提供了Lock鎖接口,提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。
// 獲取鎖
void lock()
// 釋放鎖
void unlock()
class MyRunnable implements Runnable {
private int ticket = 100;
//實現類對象實例化鎖接口
Lock lock = new ReentrantLock();
@Override
public void run() {
// 多次執行
while (true) {
// 獲取鎖
lock.lock();
if (ticket > 0) {
try {
// 線程休眠模擬網絡延遲
Thread.sleep(000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ ticket-- + "張票");
}
// 釋放鎖
lock.unlock();
}
}
線程同步總結:
7、線程死鎖問題
死鎖就是線程間因相互等待對方資源,而不能繼續執行的情況。線程同步嵌套就非常容易產生死鎖問題。
修改之前的代碼:
class MyRunnable implements Runnable {
private int ticket = 100;
// 鎖對象1
private Object object1 = new Object();
// 鎖對象2
private Object object2 = new Object();
@Override
public void run() {
// 多次執行
while (true) {
if (ticket > 0) {
if (ticket % 2 == 0) {
synchronized (object1) {// 這裏出現線程死鎖
synchronized (object2) {
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + ticket-- + "張票");
}
}
} else {
synchronized (object2) {// 這裏出現線程死鎖
synchronized (object1) {
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + ticket-- + "張票");
}
}
}
}
}
}
該程序因爲進行了同步嵌套所以會線程死鎖問題,會出現輸出不全的情況。
注:那麼如何解決死鎖問題呢?給資源排序,在所有的線程中,決定次序並始終遵照這個次序獲取鎖。
8、線程間通信問題
1.生產消費模式
多線程間除了線程安全問題外,還存在線程間通信問題。比如線程A執行完,操作了某些數據,而線程B需要這些數據做某些操作,這時線程A需要通知線程B已經有數據了,然後線程B做某些操作,當沒有數據時線程B需要反饋給線程A。線程A和線程B之間就冥冥中存在了聯繫,這就是生成消費模式。
這個類似於Android中的Work線程和UI線程通過Handler通信一樣,而java則提供了等待喚醒機制進行線程通信。
2.等待喚醒機制
Object類提供了notify、wait方法,對線程進行喚醒和等待。
// 喚醒在此對象監視器上等待的單個線程
void notify()
// 喚醒在此對象監視器上等待的所有線程
void notifyAll()
// 在其他線程調用此對象的notify()方法或 notifyAll()方法前,導致當前線程等待
void wait()
// 在其他線程調用此對象的notify()方法或 notifyAll()方法,或者超過指定的時間量前,導致當前線程等待
void wait(long timeout)
public class User {
String name;
int age;
boolean b = false;// 判斷是否有數據
// 設置數據
public synchronized void setUser(String name, int age)
throws InterruptedException {
if (this.b) {
//如果存在數據就等待
// 線程等待
this.wait();
} else {
//沒有數據則設置數據並喚醒某個線程
setName(name);
setAge(age);
this.b = true;
// 喚醒等待的單個線程
this.notify();
}
}
// 獲取數據
public synchronized void getUser() throws InterruptedException {
if (this.b) {
// 相當於消費數據
System.out.println(getName() + ":" + getAge());
// 消費後相當資源消失
this.b = false;
// 喚醒線程
this.notify();
} else {
// 線程等待
this.wait();
}
}
/** 實現get set方法 **/
}
public class Test {
public static void main(String[] args) throws InterruptedException {
User user = new User();
SetUserThread setUserThread = new SetUserThread(user);
GetUserThread getUserThread = new GetUserThread(user);
getUserThread.start();
setUserThread.start();
}
}
class GetUserThread extends Thread {
private User user;
public GetUserThread(User user) {
super();
this.user = user;
}
@Override
public void run() {
while (true) {
try {
user.getUser();
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
}
}
class SetUserThread extends Thread {
private User user;
public SetUserThread(User user) {
super();
this.user = user;
}
@Override
public void run() {
while (true) {
try {
user.setUser("Hello", 18);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
}
}
9、線程組的使用
Java提供了線程組ThreadGroup對線程進行統一管理和調度。
ThreadGroup:線程組就是一個線程的集合。
// 生成線程組對象,並設置名字
ThreadGroup threadGroup = new ThreadGroup("線程組一");
// 使用Thread的構造設置線程歸宿的線程組
new Thread(threadGroup, new MyRunnable(), "線程一").start();
new Thread(threadGroup, new MyRunnable(), "線程二").start();
new Thread(threadGroup, new MyRunnable(), "線程三");
// 獲取線程組名字
System.out.println(threadGroup.getName());// 線程組一
// 獲取線程組活動線程的估計數
Thread[] threads = new Thread[threadGroup.activeCount()];
// 線程組中的所有活動線程複製到指定數組中
threadGroup.enumerate(threads);
System.out.println(threads.length);//運行多次輸出 0 1 2
//通過輸出可以看到 threadGroup.activeCount()結果所固有的不精確特性
// 中斷線程組中所有線程
threadGroup.interrupt();
10、線程池的使用
線程池簡述:開啓一條線程是非常浪費資源的,因爲它涉及到要與操作系統進行交互;因此JDK5之後Java提供了線程池讓我們提高性能,線程池裏的線程執行完後,並不會死亡,而是再次回到線程池中成爲空閒狀態,等待下一個對象來使用。
相關對象
Executors:創建線程池的工廠類。
創建方法:
// 創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們
static ExecutorService newCachedThreadPool()
// 創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程
static ExecutorService newFixedThreadPool(int nThreads)
// 創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們,並在需要時使用提供的ThreadFactory創建新線程
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
// 創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程,在需要時使用提供的ThreadFactory創建新線程
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
// 創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
// 創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
//創建一個使用單個worker線程的Executor,以無界隊列方式來運行該線程
public static ExecutorService newSingleThreadExecutor()
ExecutorService:線程池管理接口,提供了線程的操作方法。
// 啓動一次順序關閉,執行以前提交的任務,但不接受新任務
void shutdown()
// 試圖停止所有正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列表
List<Runnable> shutdownNow()
// 提交一個返回值的任務用於執行,返回一個表示任務的未決結果的 Future
<T> Future<T> submit(Callable<T> task)
// 提交一個 Runnable任務用於執行,並返回一個表示該任務的 Future
Future<?> submit(Runnable task)
// 提交一個 Runnable任務用於執行,並返回一個表示該任務的 Future
<T> Future<T> submit(Runnable task, T result)
ExecutorService threadpool= Executors.newCachedThreadPool();
//提交任務
threadpool.submit(new Runnable() {
@Override
public void run() {
/**線程中執行邏輯**/
}
});
//這是實現線程的第三種方式
threadpool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
/**線程中執行邏輯**/
return null;
}
});
//停止任務
threadpool.shutdown();
注:線程池可以很大程度上提高性能,如存在多線程環境建議使用 。
更多方法查看API