多線程是Java語言的重要特性,大量應用於網絡編程和服務器端程序的開發。最常見的UI界面的底層原理、操作系統底層原理都大量使用了多線程技術。本篇中僅初步講解多線程的普通應用,並無深入剖析。由於JUC包的內容過多,過於深奧,本人水平有限,本文中也不擴展敘寫,希望在對於併發編程有更深一步的理解之後填上這個坑。
多線程的基本概念
對於線程的理解,我們需要先理解程序、進程以及線程的概念。
程序是一個靜態的概念,一般對應於操作系統中的一個可執行文件,例如,打開用於敲代碼的idea的可執行文件。打開idea可執行文件,將會加載該程序到內存中並開始執行它,於是就產生了“進程”,而我們打開了多個可執行文件,這就產生了多個進程。
對於多任務,多進程大多數人應該就特別熟悉,我們打開電腦上的任務管理器/活動監視器,我們就能看到一大堆進程,這是操作系統的一種能力,看起來可以在同一時刻運行多個程序。例如,我們在敲代碼的時候能同時用音樂軟件聽歌。而如今,人們往往都有多CPU多計算機,但是併發執行的進程數目並不受限於CPU數目。操作系統會爲每個進程分配CPU的時間片,給人並行處理的感覺。
多線程程序在更低一層擴展了多任務多概念:單個程序看起來在同時完成多個任務。每個任務在一個線程中執行,線程是控制線程的簡稱。如果一個程序可以同時運行多個線程,則稱這個程序是多線程的程序。
而多線程和多進程的本質區別在於每個進程都擁有自己的一套變量,而線程則共享數據。而這樣就會涉及線程安全的問題,下文會介紹這個問題。不過對於共享變量使線程之間的通信比進程之間的通信更有效、更容易。此外,在操作系統中,與進程相比較,線程更“輕量級”,創建、撤銷一個線程比啓動新進程的開銷要小得多,所以線程又被稱爲輕量級進程。
Java中如何實現多線程
Java中使用多線程非常的簡單。下文將會介紹如何創建和使用線程。
通過繼承Thread類實現多線程
繼承Thread類實現多線程的步驟如下:
- 在Java中負責實現線程功能的類是java.lang.Thread類。
- 可以通過創建Thread的實例來創建新的線程。
- 每個線程都是通過某個特定的Thread對象所對應的方法run()來完成其操作的,方法run()稱爲線程體。
- 通過調用Thread類的start()方法來啓動一個線程。
可以參考以下代碼理解:
/**
* 創建線程的方式一:
* 1.創建:繼承Thread並且重寫run方法
* 2.啓動:創建子類對象並且運行start方法
* @author Eddie
*
*/
public class StartThread extends Thread {
//程序入口點
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("一邊聽歌......");
}
}
public static void main(String[] args) {
//創建子類對象
StartThread st = new StartThread();
//啓動線程
st.start(); //不保證立即運行,靠cpu調用
// st.run(); //僅調用普通的run方法
for (int i = 0; i < 20; i++) {
System.out.println("一邊敲代碼......");
}
}
}
這種方法的缺點是:如果類已經繼承一個類,則無法繼承Thread類(Java只能繼承一個父類)。
通過Runnable接口實現多線程
在實際開發中,更多的是通過Runnable接口實現的多線程。這種方式完美解決了繼承Thread類的缺點,在實現Runnable接口的同時還可以繼承某個類。所以實現Runnable接口的方式要通用一些。
可以參考以下代碼理解:
/**
* 創建線程的方式二:
* 1.創建:實現Runnable並且重寫run方法
* 2.啓動:創建實現類對象和Thread對象並且運行start方法
* 推薦:避免單繼承的侷限性,優先使用接口
* 方便共享資源
* @author Eddie
*
*/
public class StartRunnable implements Runnable {
//線程入口點
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("一邊聽歌......");
}
}
public static void main(String[] args) {
// //創建子類對象
// StartRunnable sr = new StartRunnable();
// Thread t=new Thread(sr);
// //啓動線程
// t.start(); //不保證立即運行,靠cpu調用
// st.run(); //僅調用普通的run方法
new Thread(new StartRunnable()).start(); //同樣可以使用匿名對象的方式來使用子類
for (int i = 0; i < 20; i++) {
System.out.println("一邊敲代碼......");
}
}
}
線程狀態和生命週期
線程狀態
一個成對象在它的生命週期內,需要經歷5個狀態,如下圖所示:
-
新生狀態
用new關鍵字建立一個線程對象後,該線程對象處於新生狀態。處於新生狀態的線程有自己的內存空間,通過調用start方法進入就緒狀態。
-
就緒狀態
處於就緒狀態的線程已經具備了運行條件,但是還沒有被分配到CPU,處於“線程就緒隊列”,等待系統爲其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會進入執行狀態。一旦獲得哦CPU,線程就進入運行狀態並自動調用其run方法。下列4種原因會導致線程進入就緒狀態:
- 新建線程:調用start()方法,進入就緒狀態。
- 阻塞線程:阻塞解除,進入就緒狀態。
- 運行線程:調用yield()方法,直接進入就緒狀態。
- 運行線程:JVM將CPU資源從本線程切換到其他線程。
-
運行狀態
在運行狀態的線程執行其run方法中的代碼,直到因調用其他方法而終止,或等待某資源產生阻塞或完成任務死亡。如果在給定的時間片內沒有執行結束,線程就會被系統換下來並回到就緒狀態,也可能由於某些“導致阻塞的事件”而進入阻塞狀態。
-
阻塞狀態
阻塞是指暫停一個線程的執行以等待某個條件發生(如其資源就緒)。有4種原因會導致阻塞:
- 執行sleep(int millsecond)方法,使當前線程休眠,進入阻塞狀態。當指定的時間到了之後,線程進入就緒狀態。
- 執行wait()方法,使當前線程進入阻塞狀態。當使用notify()方法喚醒這個線程後,它進入就緒狀態。
- 當線程運行時,某個操作進入阻塞狀態,例如執行I/O流操作(read()/write()方法本身就是阻塞的方法)。只有當引起該操作阻塞的原因消失後,線程才進入就緒狀態。
- join()線程聯合:當某個線程等待另一個線程執行結束並能繼續執行時,使用join()方法。
-
死亡狀態
死亡狀態是線程生命週期中的最後一個階段。線程死亡的原因有兩個:一個是正常運行的線程完成了它run()方法內的全部工作;另外一個是線程被強制終止,如通過執行~~stop()
或destroy()~~方法來終止一個線程(stop()/destroy()方法已經被JDK廢棄,不推薦使用)。當一個線程進入死亡狀態以後,就不能回到其他狀態了。
終止線程的常用方式
上文中提到stop()/destroy()方法已經被JDK廢棄,不推薦使用。當我們需要終止線程的時候通常的做法是提供一個boolean類型的終止變量,當這個變量置爲false時,終止線程的運行。可以參考以下代碼:
/**
* 終止線程
* 1.線程正常執行完畢/2.外部干涉,加入標識(這邊所要使用的方法)
* @author Eddie
*
*/
public class TerminateThread implements Runnable{
//加入標識 標記線程體是否可以運行
private boolean flag=true;
private String name;
public TerminateThread() {
}
public TerminateThread(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
@Override
public void run() {
int i=0;
//關聯標識
while (flag) {
System.out.println(name+"運行:"+(i++)+"次。");
}
}
//對外提供改變標識的方法。
public void stop() {
this.flag=false;
}
public static void main(String[] args) {
TerminateThread tt = new TerminateThread("線程"); //新生狀態
new Thread(tt).start(); //就緒狀態
for (int i = 0; i < 99; i++) {
System.out.println("主線程運行了:"+i+"次。");
if (i==66) {
System.out.println(tt.getName()+"STOP!");
tt.stop();
}
}
}
}
暫停線程執行的常用方法
暫停線程的常用方法有sleep()和yield(),這兩個方法的區別如下:
- sleep()方法可以讓正在運行的線程進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。
- yield()方法可以讓正在運行的線程直接進入就緒狀態,讓出CPU的使用權。
sleep()方法使用的示範代碼:
public class BlockedSleep {
public static void main(String[] args) {
StateThread t1 = new StateThread();
StateThread t2 = new StateThread();
t1.start();
t2.start();
}
}
//這裏爲了簡潔實用繼承的方式實現多線程
class StateThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(1000); //調用線程的sleep()方法
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
yield()方法使用的示範代碼:
public class BlockedYield {
public static void main(String[] args) {
StateThread t1 = new StateThread();
StateThread t2 = new StateThread();
t1.start();
t2.start();
}
}
//這裏爲了簡潔實用繼承的方式實現多線程
class StateThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 99; i++) {
System.out.println(this.getName() + ":" + i);
Thread.yield(); //調用線程的yield()方法
}
}
}
以上代碼可以自己copy進IDE運行看下運行結果,sleep()方法中我們可以感覺到每條結果輸出之前的延遲,這是因爲Thread.sleep(1000)語句在起作用。而在yield()方法中,代碼可以引起線程的切換,但運行沒有明顯延遲。
聯合(合併)線程的使用方法
線程A運行期間,可以調用線程B的join()方法,讓線程B和線程A聯合。這樣,線程A就必須等待線程B執行完畢,才能繼續執行。用以下一個例子來說明一下join()方法的使用:
/**
* join:合併線程,插隊線程
* @author Eddie
*
*/
public class BlockedJoin02 {
public static void main(String[] args) {
new Thread(new father()).start();
}
}
class father implements Runnable{
@Override
public void run() {
System.out.println("爸爸想抽菸了。");
System.out.println("拿錢叫兒子去買菸。");
Thread sonThread=new Thread(new son());
sonThread.start();
try {
sonThread.join(); //調用join()方法
System.out.println("拿到了煙,把零錢給兒子。");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("兒子走丟了,出門找兒子。");
}
}
}
class son implements Runnable{
@Override
public void run() {
System.out.println("兒子拿了錢,出門買菸。!");
System.out.println("路過了遊戲廳。");
for (int i = 0; i <= 10; i++) {
System.out.println("在遊戲廳裏呆了"+i+"秒。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("走出遊戲廳去便利店買菸");
System.out.println("回家把煙給爸爸。");
}
}
Lambda表達式
Lambda表達式是一個可傳遞的代碼塊,可以在以後執行一次或多次。本文僅簡單介紹一下如何使用Lambda表達式,以及Lambda在多線程中的使用,更詳細的內容可以翻閱相關的書籍。
Lambda表達式的推導
public class LambdaTest01 {
//非靜態內部類
class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda2");
}
}
//靜態內部類
static class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda3");
}
}
public static void main(String[] args) {
//外部類
ILike like=new Like1();
like.lambda();
//非靜態內部類
like =new LambdaTest01().new Like2();
like.lambda();
//靜態內部類
like =new Like3();
like.lambda();
//局部內部類
class Like4 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda4");
}
}
like=new Like4();
like.lambda();
//匿名內部類
like=new ILike() {
@Override
public void lambda() {
System.out.println("I like lambda5");
}
};
like.lambda();
//Lambda表達式
like=()-> {
System.out.println("I like lambda5");
};
like.lambda();
// Lambda推導必須存在類型
// ()-> {
// System.out.println("I like lambda5");
// }.lambda();
}
}
//接口中只能有一個要實現的方法
interface ILike{
void lambda();
}
//外部類
class Like1 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda1");
}
}
Lambda表達式參數的簡化過程
public class LambdaTest02 {
public static void main(String[] args) {
ILove love=(String a)-> {
System.out.println("I like lambda-->"+a);
};
love.lambda("普通Lambda表達式");
//可以去掉參數類型
love=(a)-> {
System.out.println("I like lambda-->"+a);
};
love.lambda("去掉參數類型");
//只有一個參數括號可以省略
love=a-> {
System.out.println("I like lambda-->"+a);
};
love.lambda("省略參數括號");
//只有一行代碼可以省略花括號
love=a->System.out.println("I like lambda-->"+a);
love.lambda("省略花括號");
}
}
interface ILove{
void lambda(String a);
}
//外部類
//class Love1 implements ILove{
// @Override
// public void lambda(String a) {
// System.out.println("I like lambda-->"+a);
// }
//}
Lambda表達式返回值的簡化過程
public class LambdaTest03 {
public static void main(String[] args) {
//普通的Lambda表達式
IInsterest insterest=(int a1, int b1)-> {
System.out.println("I like lambda-->"+(a1+b1));
return a1+b1;
};
insterest.lambda(1, 1);
//去掉參數類型(去掉的話需要全部去掉,僅去掉一個不可行)
insterest=(a1, b1)-> {
System.out.println("I like lambda-->"+(a1+b1));
return a1+b1;
};
insterest.lambda(2, 2);
/*
* 有兩個參數不可省略參數的括號
* 有兩行代碼不可省略花括號
*/
//如果只有一行代碼,並且有返回值可以省略return;
insterest=(a1, b1)->a1+b1;
//返回了一個int數值
System.out.println(insterest.lambda(6, 6));
insterest=(a1, b1)->100;
//返回了一個int數值
System.out.println(insterest.lambda(100, 100));
}
}
interface IInsterest{
int lambda(int a,int b);
}
//class Insterest implements IInsterest{
// @Override
// public int lambda(int a1, int b1) {
// System.out.println("I like lambda-->"+(a1+b1));
// return a1+b1;
// }
//}
Lambda表達式簡化線程(用一次)的使用
public class LambdaThread01 {
//靜態內部類
static class Test1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("一邊聽歌1");
}
}
}
//非靜態內部類
class Test2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("一邊聽歌2");
}
}
}
public static void main(String[] args) {
//靜態內部類
new Thread(new Test1()).start();
//非靜態內部類
new Thread(new LambdaThread01().new Test2()).start();
//局部內部類
class Test3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("一邊聽歌3");
}
}
}
new Thread(new Test3()).start();
//匿名內部類
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("一邊聽歌4");
}
}
}).start();
//jdk8簡化 Lambda表達式
//因爲Thread裏只能傳入一個實現Runable接口的實現類並且Runable僅需要實現一個run()方法
new Thread(()-> {
for (int i = 0; i < 2; i++) {
System.out.println("一邊聽歌5");
}
}
).start();
for (int i = 0; i < 5; i++) {
System.out.println("66666666666");
}
}
}
使用Lambda表達式簡化多線程
/**
* 使用Lambda表達式簡化多線程
* Lambda表達式避免匿名內部類定義過多
* 其實質屬於函數式編程的概念
* @author Eddie
*
*/
public class LambdaThread02 {
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("一邊聽歌...");
}
}).start();
new Thread(()->System.out.println("正在學習Lambda表達式")).start();
for (int i = 0; i < 10; i++) {
System.out.println("一邊寫代碼...");
}
}
}
線程的常用方法
線程也是對象,系統爲線程定義了很多方法、優先級、名字等,以便對多線程進行有效地管理。
線程常用的方法
線程的常用方法如下表所示:
方法 | 功能 |
---|---|
getState() | 獲得線程當前的狀態 |
isAlive() | 判斷線程是否還“活着”,即線程是否還未終止 |
getPriority() | 獲得線程的優先級數值 |
setPriority() | 設置線程的優先級數值 |
setName() | 給線程設置一個名字 |
getName() | 獲得線程的名字 |
currentThread() | 取得當前正在運行的線程對象,也就是取得自己本身 |
setDaemon(boolean on) | 將線程設置成守護線程 |
使用getState()方法觀察線程狀態
public class AllState {
public static void main(String[] args) {
Thread t=new Thread(()->{
for (int i = 0; i <5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("模擬線程");
}
});
//觀察狀態
State state=t.getState();
System.out.println(state); //NEW
t.start();
state=t.getState();
System.out.println(state); //RUNNABLE
// while (state!=State.TERMINATED) {
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// state=t.getState();
// System.out.println(state); //TIMED_WAITING
// }
while (true) {
//活動的線程數
int threadNum=Thread.activeCount();
if (threadNum==1) {
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
state=t.getState();
System.out.println(state); //TIMED_WAITING
}
state=t.getState();
System.out.println(state); //TERMINATED
}
}
線程的優先級
/**
* 線程的優先級1-10
* 1.NORM_PRIORITY 5 默認
* 2.MIN_PRIORITY 1
* 3.MAX_PRIORITY 10
* 概率,不代表絕對的先後順序
* @author Eddie
*
*/
public class PriorityTest {
public static void main(String[] args) {
MyPriority mp=new MyPriority();
Thread t1=new Thread(mp,"百度");
Thread t2=new Thread(mp,"阿里");
Thread t3=new Thread(mp,"騰訊");
Thread t4=new Thread(mp,"頭條");
Thread t5=new Thread(mp,"美團");
Thread t6=new Thread(mp,"滴滴");
//設置優先級需要在線程啓動前
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(Thread.MIN_PRIORITY);
t5.setPriority(Thread.MIN_PRIORITY);
t6.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
Thread.yield();
}
}
其他方法的示例
/**
* 其他方法
* isAlive:線程是否還或者
* Thread.currentThread():當前線程
* setName.getName:設置和獲取代理線程的名稱
* @author Eddie
*
*/
public class InfoTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().isAlive());
MyInfo myInfo=new MyInfo("戰鬥機");
Thread t=new Thread(myInfo);
t.setName("公雞");
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.isAlive());
}
}
class MyInfo implements Runnable{
private String name;
public MyInfo(String name) {
super();
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+name);
}
}
守護線程
/**
* 守護線程:是爲用戶線程服務的;JVM停止不用等待守護線程執行完畢
* 線程默認用戶線程 JVM等待用戶線程執行完畢纔會停止
* @author Eddie
*
*/
public class DaemonTest {
public static void main(String[] args) {
God god=new God();
You you=new You();
Thread t=new Thread(god);
t.setDaemon(true); //將用戶線程設置爲守護線程
t.start();
new Thread(you).start();
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 1; i <=365*100; i++) {
System.out.println("Happy Life"+i+"days.");
}
System.out.println("die...");
}
}
class God implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("Bless you...");
}
}
}
線程同步
在處理多線程問題時,如果多個線程同時訪問同一個對象,並且某些線程還想修改這個對象時,就需要用到“線程同步”機制。加入線程同步後,我們稱爲這是線程安全的;線程安全在併發時保證數據的準確性、效率儘可能高。
線程同步的概念
線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程繼續使用。
用一個取款機的例子來看下未使用線程同步的情況下會發生的情況:
public class UnsafeTest02 {
public static void main(String[] args) {
Account account=new Account(100, "百萬賬戶");
ATM atm01=new ATM(account, 80);
ATM atm02=new ATM(account, 70);
new Thread(atm01,"自己").start();
new Thread(atm02,"老婆").start();
}
}
//賬戶
class Account{
private int total_assets; //賬戶總資產
private String account_name; //賬戶名字
public Account(int total_assets, String account_name) {
super();
this.total_assets = total_assets;
this.account_name = account_name;
}
public int getTotal_assets() {
return total_assets;
}
public void setTotal_assets(int total_assets) {
this.total_assets = total_assets;
}
public String getAccount_name() {
return account_name;
}
public void setAccount_name(String account_name) {
this.account_name = account_name;
}
}
//模擬取款
class ATM implements Runnable{
private Account account; //取款賬戶
private int withdrawMoney; //取款金額
private int pocketMoney; //口袋的錢
public ATM(Account account, int withdrawMoney) {
super();
this.account = account;
this.withdrawMoney = withdrawMoney;
}
@Override
public void run() {
if (account.getTotal_assets()<withdrawMoney) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setTotal_assets(account.getTotal_assets()-withdrawMoney);
pocketMoney+=withdrawMoney;
System.out.println(Thread.currentThread().getName()+":"+pocketMoney);
System.out.println(account.getAccount_name()+":"+account.getTotal_assets());
}
}
由於沒有使用線程同步機制,即使我們在線程中判斷了剩餘餘額,但是同樣會使兩個人都取款成功,這就叫做線程不安全。
實現線程同步
由於同一進程的多個線程共享同一塊存儲空間,這在帶來方便的同時,也帶來了訪問衝突問題。Java語言提供了專門機制來解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問造成的問題。這套機制就是使用synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊。
-
synchronized方法
通過在方法聲明中加入synchronized關鍵字來聲明此方法,語法格式如下:
public synchronized void accessVal(int newVal);
synchronized方法控制對“對象的類成員變量”的訪問:每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則所屬線程阻塞。方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。
-
synchronized塊
synchronized方法的缺陷是,若將一個大的方法聲明爲synchronized將會大大影響程序的工作效率。
爲此,Java提供了更好的解決辦法,就是使用synchronized塊。synchronized塊可以讓人們精確地控制具體的“成員變量”,縮小同步的範圍,提高效率。且synchronized塊可以指定鎖的對象,synchronized方法則只能鎖本對象。
通過synchronized關鍵字可聲明synchronized塊,語法格式如下:
synchronized(synObject){ //允許訪問控制的代碼 }
將以上取款機的例子加入線程同步:
public class SynBlock01 {
public static void main(String[] args) {
Account account=new Account(200, "百萬賬戶");
SynATM my = new SynATM(account,80);
SynATM wife = new SynATM(account,90);
new Thread(my,"自己").start();
new Thread(wife,"妻子").start();
}
}
//賬戶
class Account{
private int total_assets; //賬戶總資產
private String account_name; //賬戶名字
public Account(int total_assets, String account_name) {
super();
this.total_assets = total_assets;
this.account_name = account_name;
}
public int getTotal_assets() {
return total_assets;
}
public void setTotal_assets(int total_assets) {
this.total_assets = total_assets;
}
public String getAccount_name() {
return account_name;
}
public void setAccount_name(String account_name) {
this.account_name = account_name;
}
}
class SynATM implements Runnable{
private Account account;
private int drawingMoney;
private int money;
public SynATM(Account account, int drawingMoney) {
super();
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//提高性能,判斷賬戶是否有錢或者取的錢是否超過賬戶餘額,滿足條件直接返回,不需要運行同步塊
if (account.getTotal_assets()<=0 || account.getTotal_assets()<drawingMoney) {
return;
}
//同步塊:目標鎖定account
synchronized (account) {
account.setTotal_assets(account.getTotal_assets()-drawingMoney);
money+=drawingMoney;
System.out.println(Thread.currentThread().getName()+"錢包餘額:"+money);
System.out.println(account.getAccount_name()+"餘額:"+account.getTotal_assets());
}
}
}
synchronized (account)意味着線程需要獲得account對象的“鎖”纔有資格運行同步塊中的代碼。Account對象的“鎖”也稱爲“互斥鎖”,在同一時刻只能被一個線程使用。A線程擁有鎖,則可以調用“同步塊”中的代碼;B線程沒有鎖,則進入account對象的“鎖池隊列”等待,直到A線程使用完畢釋放了account對象的鎖,B線程得到鎖纔可以調用“同步塊”中的代碼。
synchronized方法、synchronized塊和線程不安全的例子
以下是買票的例子:
public class SynBlock03 {
public static void main(String[] args) {
Syn12306 web12306 = new Syn12306();
new Thread(web12306,"黃牛").start();
new Thread(web12306,"yellow牛").start();
new Thread(web12306,"ticket_scalper").start();
}
}
class Syn12306 implements Runnable{
//票數
private int ticketNums=10;
private boolean flag=true;
@Override
public void run() {
while (flag) {
test5();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//線程安全,範圍太大-->性能效率低下:同步方法,鎖定的是SynWeb對象
public synchronized void test1() {
if (ticketNums<=0) {
flag=false;
return;
}
//模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
//線程安全,範圍太大-->性能效率低下:同步塊,鎖定this對象,即SynWeb對象
public void test2() {
synchronized(this) {
if (ticketNums<=0) {
flag=false;
return;
}
//模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//線程不安全:同步塊,鎖定ticketNums對象的屬性在變
public void test3() {
synchronized((Integer)ticketNums) {
if (ticketNums<=0) {
flag=false;
return;
}
//模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//線程不安全:同步塊
public void test4() {
//僅鎖定下面一部分,線程不安全
synchronized(this) {
if (ticketNums<=0) {
flag=false;
return;
}
}
//模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
//線程安全:儘可能鎖定合理的範圍(不是指代碼 指數據的完整性)
//double checking
public void test5() {
if (ticketNums<=0) { //考慮的是沒有票的情況
flag=false;
return;
}
//僅鎖定下面一部分,線程不安全
synchronized(this) {
if (ticketNums<=0) { //考慮的是最後一張票的情況
flag=false;
return;
}
//模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
}
死鎖及解決方案
死鎖的概念
“死鎖”指的是多個線程各自佔有一些共享資源,並且互相等待得到其他線程佔有的資源才能繼續,從而導致兩個或者多個線程都在等待對方釋放資源,停止執行的情形。
因此,某一個同步塊需要同時擁有“兩個以上對象的鎖”時,就可能會發生“死鎖”的問題。用以下一個例子來描述下死鎖的形成:
public class DeadLock {
public static void main(String[] args) {
new Thread(new MarkUp("大丫", true)).start();
new Thread(new MarkUp("二丫", false)).start();
}
}
//鏡子
class Mirror{
}
//口紅
class Lipstick{
}
//化妝
class MarkUp implements Runnable{
//不管幾個對象只有一份
static Mirror mirror=new Mirror();
static Lipstick lipstick=new Lipstick();
private String girl;
private boolean flag;
public MarkUp(String girl, boolean flag) {
this.girl = girl;
this.flag = flag;
}
@Override
public void run() {
markup();
}
//相互持有對方的對象鎖
private void markup() {
if (flag) {
synchronized (mirror) { //先將鏡子鎖上
System.out.println(this.girl+"照鏡子。");
//1秒後,塗口紅
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick) { //然後將口紅鎖上
System.out.println(this.girl+"塗口紅。");
}
}
}else {
synchronized (lipstick) { //先將口紅鎖上
System.out.println(this.girl+"塗口紅。");
//2秒後,照鏡子
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror) { //然後將鏡子鎖上
System.out.println(this.girl+"照鏡子。");
}
}
}
}
}
執行後,兩個線程都在等對方的資源,都處於停滯狀態。
死鎖的解決方法
死鎖是由於“同步塊需要同時持有多個對象鎖”造成的。要解決這個問題,就是同一個代碼塊不要同時持有兩個對象鎖。如上面的死鎖例子,可以修改如下:
public class DeadLock {
public static void main(String[] args) {
new Thread(new MarkUp("大丫", true)).start();
new Thread(new MarkUp("二丫", false)).start();
}
}
//鏡子
class Mirror{
}
//口紅
class Lipstick{
}
//化妝
class MarkUp implements Runnable{
//不管幾個對象只有一份
static Mirror mirror=new Mirror();
static Lipstick lipstick=new Lipstick();
private String girl;
private boolean flag;
public MarkUp(String girl, boolean flag) {
this.girl = girl;
this.flag = flag;
}
@Override
public void run() {
markup();
}
//相互持有對方的對象鎖
private void markup() {
if (flag) {
synchronized (mirror) { //先將鏡子鎖上
System.out.println(this.girl+"照鏡子。");
//1秒後,塗口紅
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
synchronized (lipstick) { //然後將口紅鎖上
System.out.println(this.girl+"塗口紅。");
}*/
}
synchronized (lipstick) { //然後將口紅鎖上
System.out.println(this.girl+"塗口紅。");
}
}else {
synchronized (lipstick) { //先將口紅鎖上
System.out.println(this.girl+"塗口紅。");
//2秒後,照鏡子
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
synchronized (mirror) { //然後將鏡子鎖上
System.out.println(this.girl+"照鏡子。");
}*/
}
synchronized (mirror) { //然後將鏡子鎖上
System.out.println(this.girl+"照鏡子。");
}
}
}
}
題外內容(與線程同步有相關性)
以下內容與線程同步有相關性,僅寫了幾個例子來描述。
CAS:比較並交換
public class CAS {
//庫存
private static AtomicInteger stock=new AtomicInteger(5);
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(new Customer()).start();
}
}
public static class Customer implements Runnable{
@Override
public void run() {
synchronized (stock) {
//模擬延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer left=stock.get();
if (left<1) {
System.out.println(Thread.currentThread().getName()+"沒搶到,沒有庫存了");
return;
}
System.out.println(Thread.currentThread().getName()+"搶到了,第"+left+"件商品,剩餘"+left+"件商品。");
stock.set(left-1);
}
}
}
}
指令重排
public class HappenBefore {
private static int a=0; //變量1
private static boolean flag=false; //變量2
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
a=0;
flag=false;
//線程1 讀取數據
Thread t1=new Thread(()->{
a=1;
flag=true;
});
//線程2 更改數據
Thread t2=new Thread(()->{
if (flag) {
a*=1;
}
//指令重排
if (a==0) {
System.out.println("Happen before,a->"+a);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
}
可重入鎖:鎖可以延續使用
public class LockTest {
public void test() {
//第一次獲得鎖
synchronized (this) {
while (true) {
//第二次獲得同樣的鎖
synchronized (this) {
System.out.println("ReentrantLock");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new LockTest().test();
}
}
不可重入鎖:鎖不可以延續使用
public class LockTest01 {
Lock lock=new Lock();
public void a() {
lock.lock();
doSomething();
lock.unLock();
}
//不可重入
public void doSomething() {
lock.lock();
//............
lock.unLock();
}
public static void main(String[] args) {
new LockTest01().a();
new LockTest01().doSomething();
}
}
class Lock{
//是否佔用
private boolean isLocked=false;
//使用鎖
public synchronized void lock() {
while (isLocked) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked=true;
}
//釋放鎖
public synchronized void unLock() {
isLocked=false;
notify();
}
}
volatile關鍵字
volatile用於保證數據的同步,也就是可見性(不保證原子性),可以參考以下例子:
public class ValatileTest {
private volatile static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0) { //此處不要編寫代碼
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
}
}
線程併發協作(生產者-消費者模式)
生產者-消費者模式的基本概念
多線程環境下,經常需要多個線程能夠併發和協作。這是,就需要了解一個重要的多線程併發協作模型“生產者-消費者模式”;
- 什麼是生產者。生產者指的是負責生產數據的模塊(這裏的模塊指的可能是方法、對象、線程、進程等)。
- 什麼是消費者。消費者指的是負責處理數據的模塊(這裏的模塊指的可能是方法、對象、線程、進程等)。
- 什麼是緩衝區。消費者不能直接使用生產者的數據,它們之間有個“緩衝區”。生產者將生產好數據放入“緩衝區”,消費者從“緩衝區”拿出要處理的數據。
緩衝區是實現併發操作的核心。緩衝區設置有如下3個好處:
- 實現線程的併發協作:有了緩衝區以後,生產者線程只需要往緩衝區裏面放置數據,而不需要管消費者消費的情況;同樣,消費者只需要從緩衝區拿出數據處理即可,不需要考慮生產者生產的情況。這樣,就從邏輯上實現了“生產者線程”和“消費者線程”的分離。
- 解耦了生產者和消費者。生產者不需要和消費者直接打交道。
- 解決忙閒不均,提高效率。生產者生產數據慢時,但在緩衝區仍有數據,不影響消費者消費;消費者處理數據慢時,生產者仍然可以繼續往緩衝區裏面放置數據。
而生產者-消費者模式主要有兩種實現方法:管程法以及信號燈法。
線程併發協作(線程通信)的使用情景
- 生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互爲條件。
- 對於生產者,沒有生產產品之前,消費者要進入等待狀態。而生產了產品之後,又需要馬上通知消費者消費。
- 對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新產品以供消費。
- 在生產者-消費者問題中,僅適用synchronized是不夠的。synchronized可以阻止併發更新同一個共享資源,雖然實現了同步,但它不能用來實現不同線程之間的消息傳遞(通信),這就需要用到線程通信的方法了。
線程通信的常用方法
方法名 | 作用 |
---|---|
final void wait() | 表示線程一直等待,直到得到其他線程通知 |
void wait(long timeout) | 線程等待指定毫秒參數的時間 |
final void wait(long timeout,int nanos) | 線程等待指定毫秒、微秒的時間 |
final void notify() | 喚醒一個處於等待狀態的線程 |
final void notifyAll() | 換新同一個對象上所有調用wait()方法的線程,優先級別高的線程優先運行 |
- 注意事項: 以上方法均是java.lang.Object類的方法,只能在同步方法或者同步塊中使用,否則會拋出異常。
在實際開發中,尤其是“架構設計”中會大量使用“生產者-消費者”模式。初學者僅需瞭解作用即可,如果想深入理解架構這一部分內容是相當重要的。
生產者消費者實現方法
以下是生產者-消費者模式的實現方法的實例,可結合概念以及註釋理解。
管程法
public class CoTest01 {
public static void main(String[] args) {
SynContainer container=new SynContainer();
new Thread(new Producer(container)).start();
new Thread(new Consumer(container)).start();
}
}
//生產者
class Producer implements Runnable{
private SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
//生產
for (int i = 0; i < 100; i++) {
System.out.println("生產第"+(i+1)+"個麪包");
container.push(new Bread(i));
}
}
}
//消費者
class Consumer implements Runnable{
private SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
//消費
for (int i = 0; i < 100; i++) {
System.out.println("買了"+(container.get().getId()+1)+"個麪包");
}
}
}
//緩衝區
class SynContainer{
Bread[] breads=new Bread[10];
private int count =0;
//存儲 生產
public synchronized void push(Bread bread) {
//緩衝區(庫存)滿了停止消費
if (count==breads.length) {
try {
this.wait(); //線程阻塞 停止生產,消費者通知生產解除阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//容器未滿可以生產
breads[count]=bread;
count++;
//this.notify();
this.notifyAll(); //生產了商品可以通知生產者恢復消費了
}
//獲取 消費
public synchronized Bread get() {
//緩衝區爲空(沒有面包)就需要停止消費
if (count==0) {
try {
this.wait(); //線程阻塞 停止消費,生產者通知消費解除阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//沒有數據只能等待
count--;
//this.notify();
this.notifyAll(); //消費了商品可以通知生產者恢復生產了
return breads[count];
}
}
//麪包
class Bread{
private int id;
public int getId() {
return id;
}
public Bread(int i) {
super();
this.id = i;
}
}
信號燈法
public class CoTest02 {
public static void main(String[] args) {
Tv tv=new Tv();
new Thread(new Actor(tv)).start();
new Thread(new Audience(tv)).start();
}
}
//生產者 演員
class Actor implements Runnable{
private Tv tv;
public Actor(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0) {
this.tv.play("牛逼");
} else {
this.tv.play("666");
}
}
}
}
//消費者 觀衆
class Audience implements Runnable{
private Tv tv;
public Audience(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
//同一個資源 電視
class Tv{
private String voice;
//信號燈:true表示演員表演,觀衆等待;false表示觀衆等待,演員表演
private boolean flag=true;
public synchronized void play(String voice){
//演員等待
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演員說了:"+voice);
this.voice=voice;
this.notifyAll(); //喚醒
this.flag=!this.flag; //切換標誌
}
public synchronized void watch() {
//觀衆等待
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("觀衆聽到了:"+this.voice);
this.notifyAll(); //喚醒
this.flag=!this.flag; //切換標誌
}
}
任務定時調度
任務定時調度在項目開發中經常用到。在實際開發中可以使用quanz任務框架來開發,也可以使用Timer和Timertask類實現同樣的功能。
通過Timer和TimerTask類可以實現定時啓動某個線程,通過線程執行某個任務的功能。
Timer和Timertask類
-
java.util.Timer
在這種方式中,Timer類的作用類似於鬧鐘的功能,也就是定時或者每隔一定時間觸發一次線程。其實,Timer是JDK中提供的一個定時器工具。使用的時候會在主線程之外起一個單獨的線程執行指定的計劃任務,可以指定執行一次或者反覆執行多次,起到類似鬧鐘的作用。
-
java.util.TimerTask
TimerTask類是一個抽象類,該類實現了Runnable接口,所以該類具備多線程能力。在這種實現方式中,通過繼承TimerTask使用該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啓動線程的執行。
可以參考以下例子理解:
public class TimerTest {
public static void main(String[] args) {
Timer timer=new Timer();
//執行安排
//timer.schedule(new MyTimer(), 3000); //3000毫秒後執行1次
//timer.schedule(new MyTimer(), 3000,1000); //3000毫秒後執行,然後每隔1000毫秒執行一次
Calendar calendar=new GregorianCalendar(2020,05,06,20,45,00); //傳入一個時間(注意月份0-11)
//timer.schedule(new MyTimer(), calendar.getTime()); //按預定的時間執行一次
timer.schedule(new MyTimer(), calendar.getTime(), 1000); //按預定的時間執行,然後每隔1000毫秒執行一次
}
}
//任務類
class MyTimer extends TimerTask{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("放空大腦。。。");
}
System.out.println("----------END-------------");
}
}
在實際使用中,一個Timer可以啓動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間需要完全獨立的話,最好還是一個Timer啓動一個TimerTask。
Quartz的簡單例子
使用Quartz框架我們可以到Quartz官網下載開源文件,本文僅描述一個簡單的例子,如果想深入瞭解可以查看文件中的API文檔以及源碼。
首先我們需要一個創建一個任務的對象:
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class HelloJob implements Job {
public HelloJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException {
System.out.println("------start-------");
System.out.println("Hello World! - " + new Date());
System.out.println("------end-------");
}
}
以下是一個簡單使用例子:
import static org.quartz.DateBuilder.evenSecondDateAfterNow;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
/**
* Quartz學習入門
* @author WHZ
*
*/
public class SimpleExample {
public void run() throws Exception {
//1、創建Scheduler工廠
SchedulerFactory sf = new StdSchedulerFactory();
//2、從工廠中獲取調度器
Scheduler sched = sf.getScheduler();
//3、創建JobDetail(任務)
JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();
//時間
//Date runTime = evenMinuteDate(new Date()); //下一分鐘
Date runTime = evenSecondDateAfterNow(); //下一秒
//4、觸發器(觸發條件)
//Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime). //按設定的時間開始運行
withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build(); //間隔5秒,重複3次
//5、註冊任務和觸發條件
sched.scheduleJob(job, trigger);
//6、啓動
sched.start();
try {
//5秒後停止(該線程總共運行的時間)
Thread.sleep(30L * 1000L);
} catch (Exception e) {
}
//7、停止
sched.shutdown(true);
}
public static void main(String[] args) throws Exception {
SimpleExample example = new SimpleExample();
example.run();
}
}
實際開發中,可以使用該開源框架更加方便實現任務的定時調度,實際上該框架底層原理就是Timer和TimerTask類的內容,想要深入瞭解可以嘗試閱讀QUARTZ框架的源碼。
結語
本篇到此完結,多線程的內容在Java中是極其深奧的一部分。礙於本人水平有限,本文中沒有描述JUC包的內容,可以參考相關的API文檔以及書籍來學習。而對於更加複雜的系統級程序設計,建議參考更高級的參考文獻。希望看到這裏的讀者能點個贊給個關注,祝各位早日年薪百萬!