04 Java SE 多線程
By Kevin Song
- 04-01 多線程概述
- 04-02 JVM中的多線程
- 04-03 多線程建立
- 04-04 多線程同步
- 04-05 多線程中的單例模式
- 04-06 多線程死鎖
- 04-07 多線程通信
- 04-08 多線程停止
04-01 多線程概述
程序:正在進行中的程式
線程:程序中的一個負責程式執行的控制單元(執行路徑)
- 一個程序中可以有多個線程
- 一個程序中至少有一個線程
多線程作用:開啟多個線程是為了同時運作多部分代碼
多線程的優缺點:
- 優點:可以同時運作多個程式
- 缺點:記憶體處理到程式頻率變低,運作速度變慢
04-02 JVM中的多線程
JVM啟動時就啟動了多個線程,至少有兩個線程
- 執行主方法的線程:該線程的任務代碼都定義在主方法中
- 負責垃圾回收的線程
Object 類中的 finalize() 方法:
當垃圾回收器确定不存在該對象的更多引用時,由對象的垃圾回收器調用此方法
System類中的 System.gc() 方法:運作垃圾回收器,垃圾回收器運作時間随機
class Demo extends Object {
public void finalize() {
System.out.println("Recycle Complete");
}
}
class ThreadDemo {
public static void main(String[] args) {
new Demo();
new Demo();
System.gc(); //
new Demo();
System.out.println("Hello World!");
}
}
/*
輸出
Hello World!
Recycle Complete
Recycle Complete
因為是兩個線程,先運作主線程,JVM關閉前運作垃圾回收線程
*/
04-03 多線程建立
主方法單線程運作
不建立多線程
class Demo {
private String name;
Demo (Srting name) {
this.name = name;
}
public void show() {
for(int x = ; x < ; x++) {
//y的for循環讓每次輸出都有一定的延遲,但是必須d1都輸出完才輸出d2,這時就需要多線程來讓他們同時輸出
for(int y =-; y < ; y++) {}
System.out.println(name+"...x="+x);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d1 = new Demo("Sequence Initializing");
Demo d2 = new Demo("序列初始化中");
d1.show();
d2.show();
}
}
建立多線程
目的:開啟一條執行路徑,使得指定的代碼和其他代碼實作同時運作,而此執行路徑運作的指定代碼就是這個執行路徑的任務
任務存放位置:
- JVM建立的主線程的任務都定義在了主方法中
- 自定義線程的任務定義在Thread類的run方法中
建立多線程有兩種方法
- 繼承Thread類
- 實作Runnable接口
建立線程方式一
繼承Thread類
步驟:
- 定義一個類,繼承Thread類
- 重寫Thread類中的 run(); 方法
- 直接建立Thread類的子類對象(建立線程)
- 調用 start(); 方法開啟線程,并調用線程的任務run(); 方法
class Demo extends Thread{
private String name;
Demo (Srting name) {
this.name = name;
}
public void run() {
for(int x = ; x < ; x++) {
//currentThread()擷取目前運作中線程的引用
System.out.println(name+"...x="+x+"...Thread Name="+Thread.currentThread().getname());
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d1 = new Demo("Sequence Initializing");
Demo d2 = new Demo("序列初始化中");
d1.start();//開啟線程,調用run方法
d2.start();//開啟線程,調用run方法
//CPU在主線程,d1,d2之間随機高速切換
}
}
Thread類中的方法&線程名稱
getName()方法
擷取線程的名稱Thread-編号(從0開始)
主線程的名字main
currentThread()方法
擷取目前運作中線程的引用,該方法為靜态,可以直接被類名調用
多線程異常問題
有異常的線程結束運作,run() 方法出棧,線程之間互不影響。
線程的狀态
- new被建立
- start() 運作(具備執行資格,具備執行權)
- 當機(釋放執行權,釋放執行資格)
- sleep(time) 當機 時間到自動喚醒
- wait() 當機 notify 喚醒
- 阻塞(具備執行資格,不具備執行權,正在等待執行權)
- 消亡
- run() 方法結束
- stop() 停止線程
- 當機(釋放執行權,釋放執行資格)
- start() 運作(具備執行資格,具備執行權)
CPU的執行資格:可以被CPU處理,在處理隊列中排隊
CPU的執行權:正在被CPU處理
建立線程方式二
實作Runnable接口
步驟:
- 定義一個類,實作Runnable接口
- 重寫接口中的run方法,将線程的任務代碼封裝到run方法中
- 通過Thread類建立線程對象,将Runnable接口的子類對象作為構造方法的參數進行傳遞
- 調用線程對象的start(); 方法開啟線程
class Demo extends Fu implements Runable{ //無法繼承Thread但是需要多線程,通過接口形式完成
public void run() {
show();
}
public void show() {
for(int x = ; x < ; x++) {
System.out.println(Thread.currentThread().getName()+"..."+x);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start;
t2.start;
}
}
第二種方法的好處:
- 将線程的任務從線程的子類中分離出來,進行了單獨的封裝,按照面向對象的思想将任務封裝成對象
- 避免了java單繼承的局限性
04-04 多線程同步
賣票示例
四個線程,每個線程都賣100張票
class Ticket extends Thread{
private int num = ;
public void run() {
sale();
}
public void sale() {
while(true) {
if(num > ) {
System.out.println(Thread.currentThread().getName()+“...sale...”num--);
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
//四個對象,每個對象都有100張票
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
四個線程一起賣100張票
class Ticket implements Runnable {
private int num = ;
public void run() {
sale();
}
public void sale() {
while(true) {
if(num > ) {
System.out.println(Thread.currentThread().getName()+“...sale...”num--);
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket(); //一個對象,所有一共隻有100張票
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
線程安全問題的現象
會出現0,-1,-2張票的情況,因為線程1還沒運作到num–的時候,線程2,線程3有可能就已經加載進來,當num–運作完之後,線程還會繼續運作,這就導緻0,-1,-2的出現。
安全問題産生的原因
- 多個線程在操作共享的資料
- 操作共享資料的線程代碼有多條
當一個線程在執行操作共享資料的多條代碼過程中,其他線程參與了運算
多線程同步
将多條操作共享的資料的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。
同步代碼塊
格式
synchronized(對象) {
需要被同步的代碼;
}
class Ticket implements Runnable {
private int num = ;
Object obj = new Object; //此對象為了傳給synchronized作為鎖
public void run() {
sale();
}
public void sale() {
while(true) {
synchronized(obj) { //synchronized修飾的代碼塊隻能同一時間被一個線程調用
if(num > ) {
try {
Thread.sleep();
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName()+“...sale...”num--);
}
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket(); //一個對象,所有一共隻有100張票
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步的前提:
- 同步中必須有多個線程并使用同一個鎖
- 當作鎖的對象必須在 run() 方法外建立
同步的優缺點:
- 優點:解決了線程的安全問題
- 缺點:相對降低了效率,因為同步外的線程都會判斷同步鎖
同步方法
/*
兩個客戶每次去銀行存100,存三次
*/
class Bank {
private int sum;
private Object obj = new Object();
public synchronized void add(int num) {//同步方法
// synchronized(obj) {
sum = sum + num;
System.out.println("sum="+sum);
// }
}
}
class Cus implements Runnable {
private Bank b = new Bank();
public void run() {
for(int x = ; x < ; x++) {
b.add();
}
}
}
class BankDemo {
public static void main(String[] args) {
Cus c = new Cus;
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
/*
輸出:
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600
*/
同步代碼塊和同步方法同時使用
class Ticket implements Runnable {
private int num = ;
Object obj = new Object; //此對象為了傳給synchronized
boolean flag = true;
public void run() {
if(flag)//flag為true則運作同步代碼快
while(true) {
synchronized(this) {
if(num>) {
try {
Thread.sleep();
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName()+"...obj..."+num--);
}
}
}
else //flag為false則運作同步方法
while(true)
this.show();
}
public synchronized void show() {
if(num > ) {
try {
Thread.sleep();
} catch ()
System.out.println(Thread.currentThread().getName()+“...syn...”num--);
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket(); //一個對象,所有一共隻有100張票
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
t1.start();
try {
Thread.sleep();
} catch(InterruptedException e) {}
t.flag = false;
t2.start();
}
}
同步方法和同步代碼快的差別:
- 同步方法的鎖是固定的this(目前對象)
- 同步代碼塊的鎖是任意的對象
靜态同步方法使用的鎖:
該方法所屬位元組碼檔案對象,可以用如下方式擷取
- this.getClass() 僅限非靜态方法中使用
- Ticket.class
04-05 多線程中的單例模式
使用同步雖然可以避免安全問題,但是會導緻程式運作效率變低,因為每個線程都需要判斷是否可以拿鎖
//餓漢式(單例設計模式)
class Single {
private static final Single s = new Single();
private Single() {}
public static Single getInstance() {
return s;
}
}
//懶漢式(延遲加載單例設計模式)
class Single {
private static Single s = null;
private Single() {}
public static Single getInstance() {
if(s == null) {//解決效率問題:多加一句判斷,後續線程不會判斷是否可以拿鎖
//不用this.getClass()是因為getClass()是非靜态方法
synchronized(Single.class) {//解決安全問題
if(s == null)
s = new Single();
}
}
return s;
}
}
class SingleDemo {
public static void main(String[] args) {
System.out.println();
}
}
04-06 多線程死鎖
同步代碼塊嵌套
class Test implements Runnable{
private boolean flag;
Test(boolean flag) {
this.flag = flag;
}
public void run() {
if(flag) {
synchronized(MyLock.locka) {
System.out.println("if...locka...");
synchronized(MyLock.lockb) {
System.out.println("if...lockb...");
}
}
} else {
synchronized(MyLock.lockb) {
System.out.println("if...lockb...");
synchronized (MyLock.locka) {
System.out.println("if...locka...");
}
}
}
}
}
class MyLock {
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockTest {
public static void main(String[] args) {
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start;
t2.start;
}
}
04-07 多線程通信
定義:多個線程在處理同一資源,任務卻不同
等待喚醒機制
涉及的方法:
- wait(); 讓線程處于當機狀态,釋放執行權和執行資格,存儲到線程池中
- notify(); 喚醒線程池中一個線程
- notifyAll(); 喚醒線程池中所有線程,防止多生産多消費的死鎖問題
這些方法都必須定義在同步中,因為這些方法都是用于線程狀态的方法,必須要明确到底操作的是哪個鎖上的線程。
等待喚醒機制中的方法(wait(); notify(); notifyAll();)定義在Object類中,因為這些方法是螢幕的方法,螢幕其實就是鎖
//資源
class Resource {
private String name;
private String sex;
privateboolean flag = false; //用來判斷是否可以輸入輸出
public synchronized void set(String name) {
if(flag) //如果flag為false,則不wait
try {
this.wait();
} catch (InterruptException e) {}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out() {
if(!flag)
try {
this.wait();
} catch (InterruptException e) {}
System.out.println(name+"..."+sex);
flag = false;
notify();
}
}
//輸入
class Input implements Runnable {
Resource r;
Input(Resource r) {
this.r = r;
}
public void run() {
int x = ;
while(true) {
if(x == ) { //控制兩種資料輸入
r.set("Kevin","Male")
} else {
r.set("Lily","Female");
}
x = (x+)%;
}
}
}
//輸出
class Output implements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.out();
}
}
}
}
class ResourceDemo {
public static void mian(String[] args) {
//建立資源
Resource r = new Resource();
//建立任務
Input in = new Input(r);
Output out = new Output(r);
//建立線程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//開啟線程
t1.start();
t2.start();
}
}
多生産者多消費者問題
多生産多消費與單生産單消費的差別
flag判斷語句差別
- if隻判斷标記一次,會導緻不該運作的線程開始運作,出現資料錯誤
- while可以循環判斷标記,解決了線程擷取執行權後,是否要運作的問題
喚醒語句差別
- notify()隻能喚醒一個線程,如果喚醒了本方,沒有意義,并且會導緻死鎖,所有線程都進線程池
- notifyAll()解決了,本方線程一定會喚醒對方線程
//定義資源
class Resource {
private String name;//資源名稱
private int count = ;//計數器
private boolean flag = false;
public synchronized void set(String name) {
while(flag)//flag為false時不wait,直接運作下面内容。
try {
this.wait();//當flag為true時進入wait狀态
} catch (InterruptException e) {}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"...生産者..."+this.name);
flag = true; //flag改成true
notify(); //随機喚醒一個線程,如果是多生産多消費者則用notifyAll();
}
public synchronized void out() {
while(!flag)
try {
this.wait();
} catch (InterruptException e) {}
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag = false;
notify();//随機喚醒一個線程,如果是多生産多消費者則用notifyAll();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.set("烤鴨");
}
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.out();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(con);
t0.start();
t1.start();
}
}
JDK1.5新特性
Lock
Lock接口:代替了同步代碼快或者同步方法,将同步的隐式鎖操作變成顯式鎖操作,更為靈活,可以一個鎖上加多組螢幕。
- lock(); 擷取鎖
- unlock(); 釋放鎖,通常需要定義在finally代碼塊中
Lock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
code;
} finally {
lock.unlock();
}
}
Condition
Condition接口:代替了Object中的wait notify notifyAll方法。這些螢幕被封裝,變成Condition螢幕對象,可以任意鎖進行組合
- await(); 功能同wait()
- signal(); 同notify()
- signalAll(); 同notifyAll()
1.5版本的多生産者多消費者代碼
//定義資源
class Resource {
private String name;//資源名稱
private int count = ;//計數器
private boolean flag = false;
//建立一個鎖對象
Lock lock = new ReentrantLock();
//通過已有的鎖擷取該鎖上的螢幕對象
//Condition con = lock.newCondition();
Condition Producer_con = lock.newCondition();
Condition Consumer_con = lock.newCondition();
public void set(String name) {
lock.lock();
try {
while(flag)//flag為false時不wait,直接運作下面内容。
try {
Producer_con.await();//當flag為true時進入wait狀态
} catch (InterruptException e) {}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"...生産者..."+this.name);
flag = true; //flag改成true
//notifyAll();
//con.signalAll();
consumer_con.signal();
} finally {
lock.unlock();
}
}
public synchronized void out() {
lock.lock();
try {
while(!flag)
try {
consumer_con.await();
} catch (InterruptException e) {}
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag = false;
//notifyAll();
//con.signalAll();
//Producer_Con.signal();
} finally {
lock.unlock();
}
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.set("烤鴨");
}
}
}
class Consumer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.out();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(con);
t0.start();
t1.start();
}
}
wait和sleep差別
- 時間指定不同
- wait可以指定時間也可以不指定
- sleep**必須**指定時間
- 在同步中時,對CPU的執行權和鎖的處理不同
- wait:釋放執行權,釋放鎖
- sleep:釋放執行權,不釋放鎖
04-08 多線程停止
停止線程
- stop方法,已經過時
- 定義标記,使run方法結束
定義标記
定義一個标記flag,當flag為true時線程可以運作,當flag為負時線程停止運作。
局限性:當有wait() 的時候會導緻線程無法繼續判斷flag而程序當機
class StopThread implements Runnable {
boolean flag = true; //定義flag标記
public void run() {
while(flag) { //flag标記為true時運作run方法
System.out.println(Thread.currentThread().getName()+"...");
}
}
public void setFlag() { //定義設定flag為false的方法
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = ;
for(;;) {
if(++num==) {
st.setFlag();
break;
}
System.out.println("main..."+num);
}
System.out.println("over");
}
}
interrupt()方法
Interrput() 方法可以把線程從當機狀态強制恢複到運作狀态中來
class StopThread implements Runnable {
boolean flag = true; //定義flag标記
public synchronized void run() {
while(flag) { //flag标記為true時運作run方法
try {
wait();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getNmae()+"..."+e);
flag = false;
}
System.out.println(Thread.currentThread().getName()+"...");
}
}
public void setFlag() { //定義設定flag為false的方法
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = ;
for(;;) {
if(++num==) {
//st.setFlag();
t1.interrupt();//強制恢複運作
t2.interrupt();//強制恢複運作
break;
}
System.out.println("main..."+num);
}
System.out.println("over");
}
}
setDaemon()方法
使線程變成守護線程(背景線程),前台線程結束之後背景線程自動結束
join()方法
執行權讓給t1
t1.join();
setPriority()方法
設定線程優先級
- MAX_PRIORITY
- MIN_PRIORITY
- NORM_PRIORITY