一文詳細聊聊java的多線程
1.什么是多線程
定義:多線程是指在一個程序中同時執(zhí)行多個線程的技術。每個線程代表一個獨立的執(zhí)行路徑,但共享相同的內存空間和系統資源。
優(yōu)勢:
提高CPU利用率? :當一個線程等待I/O操作時,另一個線程可以繼續(xù)執(zhí)行
改善響應性? :用戶界面保持響應,后臺處理任務
并發(fā)處理任務? : 同時處理多個請求或計算
平時我們使用的大多都是單線程也就是main線程,雖然已經可以完成大部分的工作,但是如果任務量一旦多起來,那么你程序的吞吐量或許會指數型下降。這時候,能多點“幫手”一起完成任務就是至關重要的了,接下來我們來說下如何創(chuàng)建多線程
2.多線程的常見實現方式
2.1 繼承Thread類
這是最經典也是最簡單的實現方式,只需要自定義類繼承java.lang.Thread類,重寫其run()方法,run()方法中定義了線程執(zhí)行的具體任務。創(chuàng)建該類的實例后,通過調用start()方法啟動線程。
class MyThread extends Thread {
@Override
public void run() {
// 線程執(zhí)行的代碼
}
}
// 使用
MyThread thread1 = new MyThread();
thread1.start(); // 啟動線程使用Thread雖然簡單,但是有個非常顯而易見的缺點:由于java只支持單繼承,所以MyThread這個類不能再繼承其他的父類。
2.2實現Runnable接口
實現Runnable接口也可以創(chuàng)建多線程,并且沒有繼承Thread類的缺點,這也是開發(fā)中推薦使用的多線程實現的方式之一
class MyRunnable implements Runnable {
@Override
public void run() {
// 線程執(zhí)行的代碼
}
}
// 使用
Thread thread2 = new Thread(new MyRunnable());
thread2.start();2.3實現Callable接口與Future
java.util.concurrent.Callable接口類似于Runnable,不同點在于Callable的call()方法可以有返回值并且可以拋出異常。要執(zhí)行Callable任務,需將它包裝進一個FutureTask,因為Thread類的構造器只接受Runnable參數,而FutureTask實現了Runnable接口。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 線程執(zhí)行的代碼,這里返回一個整型結果
return 1;
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
//使用FutureTask包裝
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
try {
Integer result = futureTask.get(); // 獲取線程執(zhí)行結果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}可以看到,使用這種方法編程稍微有些復雜,所以我更推薦平時使用第二種方式去開啟線程
3.線程的并發(fā)安全問題
3.1問題拋出
在jvm的內存結構中,多線程之間有一塊共享的區(qū)域是堆內存,該區(qū)域經常存放對象、數組等,簡單來說,平時new出來的對象大部分都放在了堆內存中,此時如果我們隨意地使用多線程就會引發(fā)一個嚴重的問題——線程并發(fā)安全問題。
舉個簡單的例子:有100張票,3個線程去分,結果會如何?
//開啟三個線程
Thread thread1 = new Mythread();
Thread thread2 = new Mythread();
Thread thread3 = new Mythread();
thread1.start();
thread2.start();
thread3.start();
//主線程休眠1秒
Thread.sleep(1000);
System.out.println("執(zhí)行了"+ticket.count+"次");
我們可以清楚的觀察到不僅票的數量不是遞減的,總執(zhí)行的次數也對不上,這就是多線程環(huán)境下的并發(fā)安全問題。
3.2解析問題
問題的根本就是ticket是全局共享的,對于對象成員變量的修改,線程會先拿到值后再做修改,由于這兩步并不是同時進行的,所以會導致在一個線程做修改之前,另一個線程拿到了修改前的值,比如t1先取100,在做-1之前,t2也取到了100,兩個線程先后做-1操作,此時t3來拿值,取到了98,不僅少了99這個狀態(tài),票數100也出現了兩次,在打印的時候,由于各個線程順序的不確定性,也會出現后打印的票數比前打印的票數多的情況。
那怎么解決問題呢?
3.3 synchronized關鍵字
定義:Java語言的關鍵字,用于實現線程同步。當修飾方法或代碼塊時,同一時間僅允許一個線程執(zhí)行該同步區(qū)域,其他線程需等待當前線程釋放鎖。
synchronized就像是一把鎖,當一個線程想要執(zhí)行被synchronized修飾的方法時,就必須先拿到鎖才能執(zhí)行,之后又有線程想執(zhí)行該方法后就會被阻塞,直到鎖被釋放才能去競爭鎖,競爭成功后就可以執(zhí)行方法
synchronized有三種實現方法:
1.同步實例方法
public class BankAccount {
private int balance = 1000;
// 鎖住當前賬戶對象(this)
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}由于鎖的是同步方法,所以實際上是對這個對象(this)進行了上鎖,這就導致只有在多線程一起使用這個對象的時候才可以實現數據隔離,如果又new了一個BankAccount對象,這時候就不能實現隔離,舉個簡單的例子,把這個對象當作是一間房子,鎖同步方法僅僅只能防止別人進你家,不能防止別人進其他人家。
2.同步靜態(tài)方法
public class BankAccount {
private int balance = 1000;
// 鎖住當前賬戶對象(this)
public static synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}static修飾后的方法就是靜態(tài)方法,生命周期上升到類的級別,與類強綁定,這時候使用synchronized修飾后相當于鎖住了整個類,由于類是全局唯一的,所以就解決了創(chuàng)建多對象后無法實現數據隔離的情況了,再拿剛剛例子來說,這次別人既進不來你家,也進不去其他的房子里。萬事大吉了!
3.同步代碼塊
public class BankAccount {
private int balance = 1000;
private final Object lock = new Object(); // 專門的鎖對象
// 鎖住當前賬戶對象(this)
public void withdraw(int amount) {
synchronized(lock) { // 使用專門的鎖對象
if (balance >= amount) {
balance -= amount;
}
}
}
}public class BankAccount {
private int balance = 1000;
// 鎖住當前賬戶對象(this)
public void withdraw(int amount) {
synchronized(Object.class) { // 使用全局的類上鎖
if (balance >= amount) {
balance -= amount;
}
}
}
}相比于前兩種,這一種方法可以做到鎖的粒度更細,性能會有所提升,在synchronized()中,你既可以模擬第一種方法鎖住對象,也可以模擬第二種方法使用類去全局上鎖,兩者效果均不變。
3.4解決問題
好了,我們已經大概了解了synchronized關鍵字的使用,接下來就是解決遺留的問題了,方法很簡單,直接在buyTicket()前使用synchronized關鍵字修飾一下即可。
public synchronized static void buyTicket() {
ticketid--;
System.out.println(Thread.currentThread().getName() + "買了票,現在還剩下" + ticketid + "張");
count++;
}加上sychronized后我們再來查看結果

現在無論我們執(zhí)行多少次,結果都不會出問題了。
總結
到此這篇關于java多線程的文章就介紹到這了,更多相關java多線程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
mybatis+springboot發(fā)布postgresql數據的實現
本文主要介紹了mybatis+springboot發(fā)布postgresql數據的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-11-11
Mybatis中SqlMapper配置的擴展與應用詳細介紹(1)
這篇文章主要介紹了Mybatis中SqlMapper配置的擴展與應用(1)的相關資料,非常不錯具有參考借鑒價值,需要的朋友可以參考下2016-11-11
SiteMesh如何結合Freemarker及velocity使用
這篇文章主要介紹了SiteMesh如何結合Freemarker及velocity使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10

