一.概念引入
1.進程與線程區別
進程:一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,一個進程中可以啓動多個線程。exe就是一個進程。
進程是受操作系統管理的基本運行單元。
線程:進程中的一個執行流程,一個進程中可以運行多個線程。線程總是屬於某個線程,進程中的多個線程共享進程的內存。
注;“同時”執行是人的感覺,在線程之間實際上輪換執行。
2.並行和併發區別
並行:就是兩個任務同時運行,就是甲任務進行的同時,乙任務也在運行(需要多核CPU)。
併發:是指兩個任務都請求運行,而處理器只能接受一個任務,就把這兩個任務安排輪流進行,由於時間間隔較短,使人感覺兩個任務都在運行。
3.Java程序運行原理
Java命令會啓動Java虛擬機,啓動JVM,等於啓動了一個應用程序,也就是啓動了一個進程。該進程會自動啓動一個“主線程”,然後主線程去調用某個類的main方法。
4.JVM的啓動與多線程
JVM啓動至少啓動了垃圾回收線程和主線程,所以是多線程的。
5.線程的五個階段
新建、就緒、運行、阻塞、死亡。
6.主線程
Myeclipse運行Java Application(main方法),啓動JVM,並且加載對應的class文件。虛擬機並會從main方法開始執行的程序代碼,一直把main方法的代碼執行結束。如果在執行過程遇到循環時間比較長的代碼,那麼在循環之後的其他代碼是不會被馬上執行的。
package com.yw.web.demo2;
class Demo{
String name;
Demo(String name){
this.name = name;
}
void show() {
for (int i=1;i<=10000 ;i++ ) {
System.out.println("name="+name+",i="+i);
}
}
}
Public class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
d.show();
d2.show();
System.out.println("Hello World!");
}
}
原因是:jvm啓動後,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之爲主線程。當程序的主線程執行時,如果遇到了循環而導致程序在指定位置停留時間過長,則無法馬上執行下面的程序,需要等待循環結束後能夠執行。
那麼,能否實現一個主線程負責執行其中一個循環,再由另一個線程負責其他代碼的執行,最終實現多部分代碼同時執行的效果?
能夠實現同時執行,通過Java中的多線程技術來解決該問題。
二.多線程技術
1.初始API
Thread類
通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程序中的執行線程。Java 虛擬機允許應用程序併發地運行多個執行線程。
構造方法
常用方法
繼續閱讀,發現創建新執行線程有兩種方法。
一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。創建對象,開啓線程。run方法相當於其他線程的main方法。
另一種方法是聲明一個實現 Runnable 接口的類。該類然後實現 run 方法。然後創建Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。
創建線程方式:繼承Thread類
創建線程的步驟:
1.定義一個類繼承Thread。
2.重寫run方法。
3.創建子類對象,就是創建線程對象。
4.調用start方法,開啓線程並讓線程執行,同時還會告訴jvm去調用run方法。
測試類:
package com.yw.web.demo2;
public class Demo01 {
public static void main(String[] args) {
//創建自定義線程對象
MyThread mt = new MyThread("新的線程!");
//開啓新線程
mt.start();
//在主方法中執行for循環
for (int i = 0; i < 10; i++) {
System.out.println("main線程!"+i);
}
}
}
class MyThread extends Thread {
//定義指定線程名稱的構造方法
public MyThread(String name) {
//調用父類的String參數的構造方法,指定線程的名稱
super(name);
}
/**
* 重寫run方法,完成該線程執行的邏輯
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
線程對象調用run方法不開啓線程。僅是對象調用方法。線程對象調用start開啓線程,並讓jvm調用run方法在開啓的線程中執行。
多線程的內存圖解
以上個程序爲例,進行圖解說明:
多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。
當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。
獲取線程名稱
開啓的線程都會有自己的獨立運行棧內存,查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。
Thread.currentThread()獲取當前線程對象
Thread.currentThread().getName();獲取當前線程對象的名稱
package com.yw.web.demo2;
public class ThreadDemo {
public static void main(String[] args) {
//創建兩個線程任務
MyThread d = new MyThread("1");
MyThread d2 = new MyThread("2");
d.run();//沒有開啓新線程, 在主線程調用run方法
d2.start();//開啓一個新線程,新線程調用run方法
}
}
class MyThread extends Thread { //繼承Thread
MyThread(String name){
super(name);
}
public MyThread() {
super();
}
//複寫其中的run方法
public void run(){
for (int i=1;i<=20 ;i++ ){
System.out.println(Thread.currentThread().getName()+",i="+i);
}
}
}
通過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1……
進行多線程編程時,不要忘記了Java程序運行是從主線程開始,main方法就是主線程的線程執行內容。
Runable接口
創建線程的另一種方法是聲明實現 Runnable 接口的類。該類然後實現 run 方法。然後創建Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。
查看Runnable接口說明文檔:Runnable接口用來指定每個線程要執行的任務。包含了一個 run 的無參數抽象方法,需要由接口實現類重寫該方法。
接口中的方法
Thread類構造方法
創建線程方式:實現Runnable接口
創建線程的步驟。
1、定義類實現Runnable接口。
2、覆蓋接口中的run方法。。
3、創建Thread類的對象
4、將Runnable接口的子類對象作爲參數傳遞給Thread類的構造函數。
5、調用Thread類的start方法開啓線程。
測試類:
package com.yw.web.demo2;
public class Demo02 {
public static void main(String[] args) {
// 創建線程執行目標類對象
Runnable runn = new MyRunnable();
// 將Runnable接口的子類對象作爲參數傳遞給Thread類的構造函數
Thread thread = new Thread(runn);
Thread thread2 = new Thread(runn);
// 開啓線程
thread.start();
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main線程:正在執行!" + i);
}
}
}
class MyRunnable implements Runnable {
// 定義線程要執行的run方法邏輯
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我的線程:正在執行!" + i);
}
}
}
實現Runnable的原理
實現Runnable接口,避免了繼承Thread類的單繼承侷限性。覆蓋Runnable接口中的run方法,將線程任務代碼定義到run方法中。
創建Thread類的對象,只有創建Thread類的對象纔可以創建線程。線程任務已被封裝到Runnable接口的run方法中,而這個run方法所屬於Runnable接口的子類對象,所以將這個子類對象作爲參數傳遞給Thread的構造函數,這樣,線程對象創建時就可以明確要運行的線程的任務。
實現Runnable的好處
第二種方式實現Runnable接口避免了單繼承的侷限性,所以較爲常用。實現Runnable接口的方式,更加的符合面向對象,線程分爲兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一起。一旦創建Thread類的子類對象,既是線程對象,有又有線程任務。實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。
總結
實現Runnable接口比繼承Thread類所具有的優勢:
1適合多個相同的程序代碼的線程去處理同一個資源。
2.可以避免java中的單繼承的限制。
3.增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立。
2.進階提高
線程的匿名內部類使用
使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。
方式1:創建線程對象時,直接重寫Thread類中的run方法
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
方式2:使用匿名內部類的方式實現Runnable接口,重新Runnable接口中的run方法
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...Y...." + x);
}
}
};
new Thread(r).start();
線程狀態轉換
1.新建狀態(New):新創建了一個線程對象。
2.就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3.運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4.阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5.死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
線程調度
線程的調度
**1、調整線程優先級:**Java線程有優先級,優先級高的線程會獲得較多的運行機會。
Java線程的優先級用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:
static int MAX_PRIORITY
線程可以具有的最高優先級,取值爲10。
static int MIN_PRIORITY
線程可以具有的最低優先級,取值爲1。
static int NORM_PRIORITY
分配給線程的默認優先級,取值爲5。
Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。
每個線程都有默認的優先級。主線程的默認優先級爲Thread.NORM_PRIORITY。
線程的優先級有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先級。
JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作爲優先級,這樣能保證同樣的優先級採用了同樣的調度方式。
**2、線程睡眠:**Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒爲單位。當睡眠結束後,就轉爲就緒(Runnable)狀態。sleep()平臺移植性好。
**3、線程等待:**Object類中的wait()方法,導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 一樣。
**4、線程讓步:**Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。
**5、線程加入:**join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。
**6、線程喚醒:**Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的所有線程。
注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,我這裏就不做分享了。
線程常用方法使用
1.sleep(long millis)
sleep(long millis): 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行)
2.join()
2.join():指等待t線程終止。
使用方式:
join是Thread類的一個方法,啓動線程後直接調用,即join()的作用是:“等待該線程終止”,這裏需要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行。
Thread t = new AThread();
t.start();
t.join();
爲什麼要用join()方法
在很多情況下,主線程生成並起動了子線程,如果子線程裏要進行大量的耗時的運算,主線程往往將於子線程之前結束,但是如果主線程處理完其他的事務後,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之後再結束,這個時候就要用到join()方法了。
不加join的情況:
package com.yw.web.demo2;
/**
*@functon 多線程學習,join
*@author 安彥
*@time 2017.9.20
*/
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
super(name);
this.name=name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 線程運行開始!");
for (int i = 0; i < 5; i++) {
System.out.println("子線程"+name + "運行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 線程運行結束!");
}
}
public class Demo02 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
}
}
運行結果:
發現主線程比子線程早結束
加join
package com.yw.web.demo2;
public class Demo02 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "主線程運行開始!");
Thread1 mTh1 = new Thread1("A");
Thread1 mTh2 = new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "主線程運行結束!");
}
}
class Thread1 extends Thread {
private String name;
public Thread1(String name) {
super(name);
this.name = name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 線程運行開始!");
for (int i = 0; i < 5; i++) {
System.out.println("子線程" + name + "運行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 線程運行結束!");
}
}
運行結果:
主線程一定會等子線程都結束了才結束
3.yield()
yield():暫停當前正在執行的線程對象,並執行其他線程。
Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
package com.yw.web.demo2;
/**
* @functon 多線程學習 yield
* @author 安彥
* @time 2017.9.20
*/
class ThreadYield extends Thread {
public ThreadYield(String name) {
super(name);
}
@SuppressWarnings("static-access")
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 當i爲30時,該線程就會把CPU時間讓掉,讓其他或者自己的線程執行(也就是誰先搶到誰執行)
if (i == 30) {
this.yield();
}
}
}
}
public class Demo02 {
public static void main(String[] args) {
ThreadYield yt1 = new ThreadYield("張三");
ThreadYield yt2 = new ThreadYield("李四");
yt1.start();
yt2.start();
}
}
運行結果:
第一種情況:李四(線程)當執行到30時會CPU時間讓掉,這時張三(線程)搶到CPU時間並執行。
第二種情況:李四(線程)當執行到30時會CPU時間讓掉,這時李四(線程)搶到CPU時間並執行。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。可看上面線程狀態轉換的圖。
sleep()和yield()的區別
時間上:
sleep()使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;yield()只是使當前線程重新回到可執行狀態,所以執行yield()的重點內容線程有可能在進入到可執行狀態後馬上又被執行。
執行上:
sleep方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU 的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲退讓,它把運行機會讓給了同等優先級的其他線程另外,sleep 方法允許較低優先級的線程獲得運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 佔有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待所有較高優先級的線程運行結束,纔有機會運行。
4.setPriority()重點內容
setPriority(): 更改線程的優先級。
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
用法:
Thread1 t1 = new Thread1("t1");
Thread1 t2 = new Thread1("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
5.interrupt()
interrupt():中斷某個線程,這種結束方式比較粗暴,如果t線程打開了某個資源還沒來得及關閉也就是run方法還沒有執行完就強制結束線程,會導致資源無法關閉要想結束進程最好的辦法就是用sleep()函數的例子程序裏那樣,在線程類裏面用以個boolean型變量來控制run()方法什麼時候結束,run()方法一結束,該線程也就結束了。
6. wait()
Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){…}語句塊內。從功能上來說wait就是說線程在獲取對象鎖後,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,並繼續執行。相應的notify()就是對對象鎖的喚醒操作。但有一點需要注意的是notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。Thread.sleep()與Object.wait()二者都可以暫停當前線程,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了對象鎖的控制。
單單在概念上理解清楚了還不夠,需要在實際的例子中進行測試才能更好的理解。對Object.wait(),Object.notify()的應用的例子,
題目要求如下:
建立三個線程,A線程打印10次A,B線程打印10次B,C線程打印10次C,要求線程同時運行,交替打印10次ABC。這個問題用Object的wait(),notify()就可以很方便的解決。代碼如下:
package com.yw.web.demo2;
/**
* wait用法
* @author 安彥
* @time 2017.9.20
*/
public class Demo02 implements Runnable {
private String name;
private Object prev;
private Object self;
private Demo02(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
Demo02 pa = new Demo02("A", c, a);
Demo02 pb = new Demo02("B", a, b);
Demo02 pc = new Demo02("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //確保按順序A、B、C執行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
輸出結果:
ABCABCABCABCABCABCABCABCABCABC
先來解釋一下其整體思路,從大的方向上來講,該問題爲三線程間的同步喚醒操作,
主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循環執行三個線程。爲了控制線程執行的順序,那麼就必須要確定喚醒、等待的順序,所以每一個線程必須同時持有兩個對象鎖,才能繼續執行。一個對象鎖是prev,就是前一個線程所持有的對象鎖。還有一個就是自身對象鎖。
主要的思想就是,爲了控制執行的順序,必須要先持有prev鎖,也就前一個線程要釋放自身對象鎖,再去申請自身對象鎖,兩者兼備時打印,之後首先調用self.notify()釋放自身對象鎖,喚醒下一個等待線程,再調用prev.wait()釋放prev對象鎖,終止當前線程,等待循環結束後再次被喚醒。運行上述代碼,可以發現三個線程循環打印ABC,共10次。
程序運行的主要過程就是A線程最先運行,持有C,A對象鎖,後釋放A,C鎖,喚醒B。線程B等待A鎖,再申請B鎖,後打印B,再釋放B,A鎖,喚醒C,線程C等待B鎖,再申請C鎖,後打印C,再釋放C,B鎖,喚醒A。看起來似乎沒什麼問題,但如果你仔細想一下,就會發現有問題,就是初始條件,三個線程按照A,B,C的順序來啓動,按照前面的思考,A喚醒B,B喚醒C,C再喚醒A。但是這種假設依賴於JVM中線程調度、執行的順序。
wait和sleep區別
共同點:
1. 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,並返回。
2. wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
需要注意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麼該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻拋出InterruptedException 。
不同點:
1. Thread類的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。
sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用
4. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
所以sleep()和wait()方法的最大區別是:
sleep()睡眠時,保持對象鎖,仍然佔有該鎖;
而wait()睡眠時,釋放對象鎖。
但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)。
sleep()方法
sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;
sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖並木有被釋放,其他線程無法訪問這個對象(即使睡着也持有對象鎖)。
在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級。
wait()方法
wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還需要返還對象鎖);其他線程可以訪問;
wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。
wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。
線程常見名詞解釋
**主線程:**JVM調用程序main()所產生的線程。
當前線程:這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的進程。
後臺線程:指爲其他線程提供服務的線程,也稱爲守護線程。JVM的垃圾回收線程就是一個後臺線程。用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束
前臺線程:是指接受後臺線程服務的線程,其實前臺後臺線程是聯繫在一起,就像傀儡和幕後操縱者一樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程創建的線程默認也是前臺線程。可以通過isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。
線程類的一些常用方法:
sleep(): 強迫一個線程睡眠N毫秒。
isAlive(): 判斷一個線程是否存活。
join(): 等待線程終止。
activeCount(): 程序中活躍的線程數。
enumerate(): 枚舉程序中的線程。
currentThread(): 得到當前線程。
isDaemon(): 一個線程是否爲守護線程。
setDaemon(): 設置一個線程爲守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
setName(): 爲線程設置一個名稱。
wait(): 強迫一個線程等待。
notify(): 通知一個線程繼續運行。
setPriority(): 設置一個線程的優先級。
3.高級技術
線程池使用
線程池概念
線程池其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。
線程池主要用來解決線程生命週期開銷問題和資源不足問題。通過對多個任務重複使用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。這樣,就可以立即爲請求服務,使用應用程序響應更快。另外,通過適當的調整線程中的線程數目可以防止出現資源不足的情況。
使用線程池方式:Runnable接口
通常,線程池都是通過線程池工廠創建,再調用線程池中的方法獲取線程,再通過線程去執行任務方法。
Executors:線程池創建工廠類
public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象
ExecutorService:線程池類
Future submit(Runnable task):獲取線程池中的某一個線程對象,並執行
Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用
使用線程池中線程對象的步驟:
1.創建線程池對象
2.創建Runnable接口子類對象
3.提交Runnable接口子類對象
4.關閉線程池
代碼演示:
package com.yw.web.demo2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*
* @author ANYAN
*
*/
public class ThreadDemo {
public static void main(String[] args) {
//創建線程池對象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
//創建Runnable實例對象
MyRunnable r = new MyRunnable();
//自己創建線程對象的方式
//Thread t = new Thread(r);
//t.start(); ---> 調用MyRunnable中的run()
//從線程池中獲取線程對象,然後調用MyRunnable中的run()
service.submit(r);
//再獲取個線程對象,調用MyRunnable中的run()
service.submit(r);
service.submit(r);
//注意:submit方法調用結束後,程序並不終止,是因爲線程池控制了線程的關閉。將使用完的線程又歸還到了線程池中
//關閉線程池
//service.shutdown();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一個健身教練");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教練來了: " +Thread.currentThread().getName());
System.out.println("教我健身,交完後,教練回到了健身房");
}
}
使用線程池方式:Callable接口
Callable接口:與Runnable接口功能相似,用來指定線程的任務。其中的call()方法,用來返回線程任務執行完畢後的結果,call方法可拋出異常。
ExecutorService:線程池類
Future submit(Callable task):獲取線程池中的某一個線程對象,並執行線程中的call()方法
Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用
使用線程池中線程對象的步驟:
1.創建線程池對象
2.創建Callable接口子類對象
3.提交Callable接口子類對象
4.關閉線程池
代碼演示:
package com.yw.web.demo2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo2 {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//創建線程池對象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
//創建Callable對象
MyCallable c = new MyCallable();
//從線程池中獲取線程對象,然後調用MyRunnable中的run()
service.submit(c);
//再獲取個教練
service.submit(c);
service.submit(c);
//注意:submit方法調用結束後,程序並不終止,是因爲線程池控制了線程的關閉。將使用完的線程又歸還到了線程池中
//關閉線程池
//service.shutdown();
}
}
@SuppressWarnings("rawtypes")
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println("我要一個健身教練:call");
Thread.sleep(2000);
System.out.println("教練來了: " +Thread.currentThread().getName());
System.out.println("教我健身,交完後,教練回到了健身房");
return null;
}
}
Synchronized使用
線程安全問題
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
通過一個案例,演示線程的安全問題:
電影院要賣票,模擬電影院的賣票過程。假設要播放的電影是 “戰狼II”,本次電影的座位共100個(本場電影只能賣100張票)。
模擬電影院的售票窗口,實現多個窗口同時賣 “戰狼II”這場電影票(多個窗口一起賣這100張票)
需要窗口,採用線程對象來模擬;需要票,Runnable接口子類來模擬
測試類
package com.yw.web.demo3;
public class ThreadDemo {
public static void main(String[] args) {
//創建票對象
Ticket ticket = new Ticket();
//創建3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模擬賣票
while(true){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
運行結果:
運行結果發現:上面程序出現了問題
1.票可能出現了重複的票
2.錯誤的票 0、-1
其實,線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
線程安全處理Synchronized
java中提供了線程同步機制,它能夠解決上述的線程安全問題。
線程同步的方式有兩種:
方式1:同步代碼塊
方式2:同步方法
同步代碼塊
同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {
//可能會產生線程安全問題的代碼
}
同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。
使用同步代碼塊,對電影賣票案例中Ticket類進行如下代碼修改:
package com.yw.web.demo3;
public class ThreadDemo {
public static void main(String[] args) {
//創建票對象
Ticket ticket = new Ticket();
//創建3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖對象
Object obj = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步代碼塊
synchronized (obj){
if (ticket > 0) {
//模擬電影選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
當使用了同步代碼塊後,上述的線程的安全問題,解決了。
同步方法
同步方法:在方法聲明上加上synchronized
public synchronized void method(){
//可能會產生線程安全問題的代碼
}
同步方法中的鎖對象是 this
使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:
package com.yw.web.demo3;
public class ThreadDemo {
public static void main(String[] args) {
//創建票對象
Ticket ticket = new Ticket();
//創建3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖對象
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步方法
method();
}
}
//同步方法,鎖對象this
public synchronized void method(){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){
//可能會產生線程安全問題的代碼
}
靜態同步方法中的鎖對象是 類名.class
死鎖問題
同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程序出現無限等待,這種現象我們稱爲死鎖。這種情況能避免就避免掉。
synchronzied(A鎖){
synchronized(B鎖){
}
}
代碼演示:
package com.yw.web.demo3;
import java.util.Random;
class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定線程要執行的任務代碼
@Override
public void run() {
while(true){
if (x%2 ==0) {
//情況一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if遠望");
}
}
} else {
//情況二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else遠望");
}
}
}
x++;
}
}
}
public class ThreadDemoLock {
public static void main(String[] args) {
//創建線程任務類對象
ThreadTask task = new ThreadTask();
//創建兩個線程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//啓動線程
t1.start();
t2.start();
}
}
Lock使用
查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
Lock接口中的常用方法
Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:
package com.yw.web.demo3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo {
public static void main(String[] args) {
//創建票對象
Ticket ticket = new Ticket();
//創建3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
//共100票
int ticket = 100;
//創建Lock鎖對象
Lock ck = new ReentrantLock();
public void run() {
//模擬賣票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
ck.unlock();
}
}
}
Volatile使用
基礎概念
可見性:
可見性是一種複雜的屬性,因爲可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的線程能適時地看到其他線
程寫入的值,有時甚至是根本不可能的事情。爲了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他
線程是可見的。但是這裏需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變量a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。
在 Java 中 volatile、synchronized 和 final 實現可見性。
原子性:
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要
我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。
有序性:
Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因爲其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。
代碼演示:
package com.yw.web.demo3;
/**
* @author anyan
*/
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
NoVisibility可能會持續循環下去,因爲讀線程可能永遠都看不到ready的值。甚至NoVisibility可能會輸出0,因爲讀線程可能看到了寫入ready的值,但卻沒有看到之後寫入number的值,這種現象被稱爲“重排序”。只要在某個線程中無法檢測到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那麼就無法確保線程中的操作將按照程序中指定的順序來執行。當主線程首先寫入number,然後在沒有同步的情況下寫入ready,那麼讀線程看到的順序可能與寫入的順序完全相反。
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行進行判斷,無法得到正確的結論。
這個看上去像是一個失敗的設計,但卻能使JVM充分地利用現代多核處理器的強大性能。例如,在缺少同步的情況下,Java內存模型允許編譯器對操作順序進行重排序,並將數值緩存在寄存器中。此外,它還允許CPU對操作順序進行重排序,並將數值緩存在處理器特定的緩存中。
Volatile原理
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。
當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味着每個線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。
當一個變量定義爲 volatile 之後,將具備兩種特性:
1.保證此變量對所有的線程的可見性,這裏的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。
2.禁止指令重排序優化。有volatile修飾的變量,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什麼是指令重排序:是指CPU採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因爲它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
ThreadLocal使用
ThreadLocal主要解決的就是每個線程綁定自己的值,可以將ThreadLocal類比喻成全局存放數據的池子,池子中可以存儲每個線程的私有數據。
其實就是thread的局部變量
ThreadLocal的方法有set()、get()、remove()、initialValue(),大家可以瞭解下
Get()與null
代碼:
package com.yw.web.demo3;
public class Runn {
@SuppressWarnings("rawtypes")
public static ThreadLocal t1 = new ThreadLocal();
@SuppressWarnings("unchecked")
public static void main(String[] args) {
if (t1.get() == null) {
System.out.println("從未放過值");
t1.set("我的值");
}
System.out.println(t1.get());
System.out.println(t1.get());
}
}
運行結果:
控制檯打印結果來看:第一次調用t1對象get()方法爲null,set()後,纔有值。ThreadLocal解決的是變量在不同線程間的隔離性。不同線程中的值是可以放入ThreadLocal類中進行保存的。
線程變量的隔離性
隔離性就是線程之間誰使用誰的局部變量
代碼:
//工具類
package com.yw.web.demo3;
import java.util.Date;
public class Tools {
public static ThreadLocal<Date> t1 = new ThreadLocal<Date>();
}
import java.util.Date;
public class Runnn {
public static void main(String[] args) {
try {
ThreadA a = new ThreadA();
a.start();
Thread.sleep(1000);
ThreadB b = new ThreadB();
b.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
@Override
public void run() {
try {
for (int i = 0; i < 20; i++) {
if (Tools.t1.get() ==null) {
Tools.t1.set(new Date());
}
System.out.println("A " + Tools.t1.get().getTime());
Thread.sleep(100);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadB extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (Tools.t1.get() ==null) {
Tools.t1.set(new Date());
}
System.err.println("B " + Tools.t1.get().getTime());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結果:
A和B之間具有隔離性,各有各自所擁有的值。
4.單例模式與多線程
餓漢模式
餓漢模式說白了就是立即加載。
立即加載就是使用類的時候已經將對象創建完畢,常見的實現方法就是直接new實例化。而立即加載有”着急”,”急迫”的含義,所以也成餓漢模式。
代碼演示:
package com.yw.web.demo3;
class MyObject {
//立即加載方式(餓漢模式)
private static MyObject myObject = new MyObject();
private MyObject() {
}
public static MyObject getInstance() {
/*
* 此代碼版本爲立即加載
* 缺點:不能有其他實例變量,因爲getInstance方法沒有同步,可能存在線程不安全的問題
*
*/
return myObject;
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
運行結果:
控制檯打印的hashCode是同一個值,說明對象是同一個,也就實現了立即加載型單例設計模式。
懶漢模式
懶漢模式說白了就是延遲加載。
延遲加載就是在調用get()方法時實例才被創建,常見的實現方法就是在get()方法中進行new實例化。而延遲加載有”緩慢”,”不急迫”的含義,所以也成懶漢模式。
懶漢模式是在調用方法時實例才被創建。
存在的問題
代碼演示:
package com.yw.web.demo3;
class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
//延遲加載
if(null != myObject){
}else{
try {
Thread.sleep(3000);
myObject = new MyObject();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return myObject;
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
運行結果:
控制檯打印的hashCode不是同一個值,說明對象不是同一個,也就說明了在多線程的環境中,就會出現取出多個實例的情況,與單例模式的初衷是相背離的。創建了“多例”的結果。
如何解決這一問題呢?
解決一:聲明synchronized關鍵字
既然多個線程可以同時進入getInstance()方法,那麼只需要對getInstance()方法聲明synchronized關鍵字即可。
代碼演示:
package com.yw.web.demo3;
class MyObject {
private static MyObject myObject;
private MyObject() {
}
synchronized public static MyObject getInstance() {
//延遲加載
if(null != myObject){
}else{
try {
Thread.sleep(3000);
myObject = new MyObject();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return myObject;
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
運行結果:
此方法加入同步synchronized關鍵字得到相同實例的對象,但此種方法的運行效率非常低下,是同步運行的,下一個線程想要取得對象,則必須等上一個線程釋放鎖之後,纔可以繼續執行。
解決二:使用同步代碼塊
使用同步代碼塊配合DCL雙檢查鎖機制來實現多線程環境中的延遲加載單例設計模式。
package com.yw.web.demo3;
class MyObject {
private static MyObject myObject;
private MyObject() {
}
//使用雙檢機制來解決問題,既解決了同步代碼的異步執行性,又保證的了單例的效果
public static MyObject getInstance() {
try {// 延遲加載
if (null != myObject) {
} else {
Thread.sleep(3000);
synchronized (MyObject.class) {
if (null == myObject) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
運行結果:
使用這種方式,成功解決了懶漢模式遇到多線程的問題。
單例模式這裏就介紹兩種
注:很多知識點來源於:《Java多線程編程核心技術》一書 高洪巖編制
有興趣的可以自學一下或者共同學習討論。
提示:IDE工具玩多線程時,運行代碼後若是死循環的代碼儘量把控制檯小紅點關上。太耗資源**