多線程
理解程序、進程、線程的概念
程序可以理解為靜態(tài)的代碼
進程可以理解為執(zhí)行中的程序。
線程可以理解為進程的進一步細分,程序的一條執(zhí)行路徑
什么時候需要多線程?
- 程序需要執(zhí)行兩個或者多個任務(wù)
- 程序需要實現(xiàn)一些等待的任務(wù)時,如用戶操作,文件讀寫操作,網(wǎng)絡(luò)操作,搜索等。
- 需要一些后臺運行的程序時。
多線程的優(yōu)勢.
- 減少程序的響應(yīng)時間;
- 線程切換的花銷?。?/li>
- 使用多線程可以簡化程序的結(jié)構(gòu),使得程序便于理解和維護;
如何創(chuàng)建線程(重點)
文字表述
創(chuàng)建線程的兩種方法:
繼承Thread類
- 定義子類繼承Thread類。
- 子類中重寫Thread類中的run方法。
- 創(chuàng)建Thread子類對象,即創(chuàng)建了線程對象。
- 調(diào)用線程對象start方法:啟動線程,調(diào)用run方法
實現(xiàn)Runnable接口
1)定義子類,實現(xiàn)Runnable接口。
2)子類中重寫Runnable接口中的run方法。
3)通過Thread類含參構(gòu)造器創(chuàng)建線程對象。
4)將Runnable接口的子類對象作為實際參數(shù)傳遞給Thread類的構(gòu)造方法中。
5)調(diào)用Thread類的start方法:開啟線程,調(diào)用Runnable子類接口的run方法。
代碼表示:
繼承于Thread類
/**
* Created by 陶世磊 on 2017/8/20.
*
* @Description:
*/
class PrintNum extends Thread{
public void run(){//子線程執(zhí)行的代碼
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public PrintNum(String name){
super(name);
}
}
public class TestThread {
public static void main(String[] args) {
PrintNum p1 = new PrintNum("線程1");
PrintNum p2 = new PrintNum("線程2");
p1.setPriority(Thread.MAX_PRIORITY);//10
p2.setPriority(Thread.MIN_PRIORITY);//1
p1.start();
p2.start();
}
}
方式二:實現(xiàn)Runnable接口
/**
* Created by 陶世磊 on 2017/8/20.
*
* @Description:
*/
class SubThread implements Runnable{
public void run(){//子線程執(zhí)行的代碼
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class TestThread{
public static void main(String[] args){
SubThread s = new SubThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.setName("線程1");
t2.setName("線程2");
t1.start();
t2.start();
}
}
兩種方式的對比:聯(lián)系:class Thread implements Runnable
比較哪個好?實現(xiàn)的方式較好。
①解決了單繼承的局限性。②如果多個線程有共享數(shù)據(jù)的話,建議使用實現(xiàn)方式,同時,共享數(shù)據(jù)所在的類可以作為Runnable接口的實現(xiàn)類。
線程里的常用方法:
start() run() currentThread() getName()
setName(String name) yield() join() sleep() isAlive()
getPriority() setPriority(int i); wait() notify() notifyAll()
線程的生命周期
new runnable running block dead
可以通過getState()方法來獲取線程當(dāng)前的狀態(tài)


新生狀態(tài)
在程序中用構(gòu)造方法(new操作符)創(chuàng)建一個新線程時,如new Thread(r),該線程就是創(chuàng)建狀態(tài),此時它已經(jīng)有了相應(yīng)的內(nèi)存空間和其它資源,但是還沒有開始執(zhí)行。
就緒狀態(tài)
新建線程對象后,調(diào)用該線程的 start()方法就可以啟動線程。當(dāng)線程啟動時,線程進入就緒狀態(tài)(runnable)。由于還沒有分配CPU,線程將進入線程隊列排隊,等待 CPU 服務(wù),這表明它已經(jīng)具備了運行條件。當(dāng)系統(tǒng)挑選一個等待執(zhí)行的Thread對象后,它就會從等待執(zhí)行狀態(tài)進入執(zhí)行狀態(tài)。系統(tǒng)挑選的動作稱之為“CPU調(diào)度"。一旦獲得CPU線程就進入運行狀態(tài)并自動調(diào)用自己的run方法。
運行狀態(tài)
當(dāng)就緒狀態(tài)的線程被調(diào)用并獲得處理器資源時,線程就進入了運行狀態(tài)。此時,自動調(diào)用該線程對象的 run()方法。 run()方法定義了該線程的操作和功能。運行狀態(tài)中的線程執(zhí)行自己的run方法中代碼。直到調(diào)用其他方法或者發(fā)生阻塞而終止。
阻塞狀態(tài)
一個正在執(zhí)行的線程在某些特殊情況下,如被人為掛起或需要執(zhí)行耗時的輸入輸出操作時,將讓出 CPU 并暫時中止自己的執(zhí)行,進入堵塞狀態(tài)。在可執(zhí)行狀態(tài)下,如果調(diào)用 sleep()、 suspend()、 wait()等方法,線程都將進入堵塞狀態(tài)。堵塞時,線程不能進入排隊隊列,只有當(dāng)引起堵塞的原因被消除后,線程轉(zhuǎn)入就緒狀態(tài)。重新到就緒隊列中排隊等待,這時被CPU調(diào)度選中后會從原來停止的位置開始繼續(xù)執(zhí)行。
記?。鹤枞幌笫腔氐骄途w狀態(tài),不是運行狀態(tài)。
死亡狀態(tài)
線程調(diào)用 stop()方法、destory()方法或 run()方法執(zhí)行結(jié)束后,線程即處于死亡狀態(tài)。處于死亡狀態(tài)的線程不具有繼續(xù)運行的能力。不推薦使用stop()方法【會產(chǎn)生異?!? destory()方法【destory是強制終止,不會釋放鎖】
同步和異步的區(qū)別
線程的同步機制(重點)
前提:如果我們創(chuàng)建的多個線程,存在著共享數(shù)據(jù),那么就有可能出現(xiàn)線程的安全問題:當(dāng)其中一個線程操作共享數(shù)據(jù)時,還未操作完成另外的線程就參與進來,導(dǎo)致對共享數(shù)據(jù)的操作出現(xiàn)問題。
解決方式:要求一個線程操作共享數(shù)據(jù)時,只有當(dāng)其完成操作完成共享數(shù)據(jù),其它線程才有機會執(zhí)行共享數(shù)據(jù)。
方式一:同步代碼塊:
synchronized(同步監(jiān)視器){
//操作共享數(shù)據(jù)的代碼
}
注:
1.同步監(jiān)視器:俗稱鎖,任何一個類的對象都可以才充當(dāng)鎖。要想保證線程的安全,必須要求所有的線程共用同一把鎖!
2.使用實現(xiàn)Runnable接口的方式創(chuàng)建多線程的話,同步代碼塊中的鎖,可以考慮是this。如果使用繼承Thread類的方式,慎用this!
3.共享數(shù)據(jù):多個線程需要共同操作的變量,需要明確哪部分是操作共享數(shù)據(jù)的代碼。
方式二:同步方法:將操作共享數(shù)據(jù)的方法聲明為synchronized。
比如:public synchronized void show(){ //操作共享數(shù)據(jù)的代碼}
注:1.對于非靜態(tài)的方法而言,使用同步的話,默認鎖為:this。如果使用在繼承的方式實現(xiàn)多線程的話,慎用!
2.對于靜態(tài)的方法,如果使用同步,默認的鎖為:當(dāng)前類本身。以單例的懶漢式為例。 Class clazz = Singleton.class
總結(jié):釋放鎖:wait();
不釋放鎖: sleep() yield() suspend() (過時,可能導(dǎo)致死鎖)
死鎖:不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
死鎖是我們在使用同步時,需要避免的問題!
線程的通信:
如下的三個方法必須使用在同步代碼塊或同步方法中!
wait():當(dāng)在同步中,執(zhí)行到此方法,則此線程“等待”,直至其他線程執(zhí)行notify()的方法,將其喚醒,喚醒后繼續(xù)其wait()后的代碼
notify()/notifyAll():在同步中,執(zhí)行到此方法,則喚醒其他的某一個或所有的被wait的線程。
例題:
1.兩個線程交替打印1-100自然數(shù)
2.生產(chǎn)者、消費者的例子
代碼實現(xiàn)如下:
生產(chǎn)者消費者的例子:
package com.cuteximi;
/*
* 生產(chǎn)者/消費者問題
* 生產(chǎn)者(Productor)將產(chǎn)品交給店員(Clerk),而消費者(Customer)從店員處取走產(chǎn)品,
* 店員一次只能持有固定數(shù)量的產(chǎn)品(比如:20),如果生產(chǎn)者試圖生產(chǎn)更多的產(chǎn)品,店員會叫生產(chǎn)者停一下,
* 如果店中有空位放產(chǎn)品了再通知生產(chǎn)者繼續(xù)生產(chǎn);如果店中沒有產(chǎn)品了,店員會告訴消費者等一下,
* 如果店中有產(chǎn)品了再通知消費者來取走產(chǎn)品。
分析:
1.是否涉及到多線程的問題?是!生產(chǎn)者、消費者
2.是否涉及到共享數(shù)據(jù)?有!考慮線程的安全
3.此共享數(shù)據(jù)是誰?即為產(chǎn)品的數(shù)量
4.是否涉及到線程的通信呢?存在這生產(chǎn)者與消費者的通信
*/
class Clerk{
int product;
public synchronized void addProduct(){//生產(chǎn)產(chǎn)品
if(product >= 20){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
product++;
System.out.println(Thread.currentThread().getName() + ":生產(chǎn)了第" + product + "個產(chǎn)品");
notifyAll();
}
}
public synchronized void consumeProduct(){//消費產(chǎn)品
if(product <= 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println(Thread.currentThread().getName() + ":消費了第" + product + "個產(chǎn)品");
product--;
notifyAll();
}
}
}
class Producer implements Runnable{//生產(chǎn)者
Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
public void run(){
System.out.println("生產(chǎn)者開始生產(chǎn)產(chǎn)品");
while(true){
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable{//消費者
Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
public void run(){
System.out.println("消費者消費產(chǎn)品");
while(true){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class TestThread {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Thread t1 = new Thread(p1);//一個生產(chǎn)者的線程
Thread t3 = new Thread(p1);
Thread t2 = new Thread(c1);//一個消費者的線程
t1.setName("生產(chǎn)者1");
t2.setName("消費者1");
t3.setName("生產(chǎn)者2");
t1.start();
t2.start();
t3.start();
}
}
交替打印的例子:
package com.cuteximi;
//線程通信。如下的三個關(guān)鍵字使用的話,都得在同步代碼塊或同步方法中。
//wait():一旦一個線程執(zhí)行到wait(),就釋放當(dāng)前的鎖。
//notify()/notifyAll():喚醒wait的一個或所有的線程
//使用兩個線程打印 1-100. 線程1, 線程2 交替打印
class PrintNum implements Runnable {
int num = 1;
Object obj = new Object(); //鎖
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (num <= 100) {
try {//延遲
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":"
+ num);
num++;
} else {
break;
}
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class TestThread {
public static void main(String[] args) {
PrintNum p = new PrintNum();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.setName("甲");
t2.setName("乙");
t1.start();
t2.start();
}
}
start() run()
如果線程直接調(diào)用run()方法,這會被當(dāng)成一個普通的函數(shù)來使用,這時,只有一個主線程,
然而start()可以異步的調(diào)用run()方法,來實現(xiàn)多線程。