多線程相關(guān)總結(jié):
萬字圖解Java多線程
Java(Android)線程池
面試官:說說多線程并發(fā)問題
一、概述:
1、線程是什么呢?
我們先來說一說比較熟悉的進(jìn)程吧,之后就比較容易理解線程了。所謂進(jìn)程,就是一個(gè)正在執(zhí)行(進(jìn)行)中的程序。每一個(gè)進(jìn)程的執(zhí)行都有一個(gè)執(zhí)行順序,或者說是一個(gè)控制單元。簡單來說,就是你做一件事所要進(jìn)行的一套流程。線程,就是進(jìn)程中的一個(gè)獨(dú)立的控制單元;也就是說,線程是愛控制著進(jìn)程的執(zhí)行。一個(gè)進(jìn)程至少有一個(gè)線程,并且線程的出現(xiàn)使得程序要有效率。打個(gè)比方說,在倉庫搬運(yùn)貨物,一個(gè)人搬運(yùn)和五個(gè)人搬運(yùn)效率是不一樣的,搬運(yùn)貨物的整個(gè)程序,就是進(jìn)程;每一個(gè)人搬運(yùn)貨物的過程,就是線程。
2、java中的線程:
在java中,JVM虛擬機(jī)啟動時(shí),會有一個(gè)進(jìn)程為java.exe,該程序中至少有一個(gè)線程負(fù)責(zé)java程序的執(zhí)行;而且該程序運(yùn)行的代碼存在于main方法中,該線程稱之為主線程。其實(shí),JVM啟動時(shí)不止有一個(gè)線程(主線程),由于java是具有垃圾回收機(jī)制的,所以,在進(jìn)程中,還有負(fù)責(zé)垃圾回收機(jī)制的線程。
3、多線程的意義:
透過上面的例子,可以看出,多線程有兩方面的意義:
1)提高效率。【運(yùn)行更快,CPU資源利用更充分】
2)清除垃圾,解決內(nèi)存不足的問題。
二、自定義線程:
線程有如此的好處,那要如何才能通過代碼自定義一個(gè)線程呢?其實(shí),線程是通過系統(tǒng)創(chuàng)建和分配的,java是不能獨(dú)立創(chuàng)建線程的;但是,java是可以通過調(diào)用系統(tǒng),來實(shí)現(xiàn)對進(jìn)程的創(chuàng)建和分配的。java作為一種面向?qū)ο蟮木幊陶Z言,是可以將任何事物描述為對象,從而進(jìn)行操作的,進(jìn)程也不例外。我們通過查閱API文檔,知道java提供了對線程這類事物的描述,即Thread類。創(chuàng)建新執(zhí)行線程有兩種方法:
1、創(chuàng)建線程方式
方式一:繼承Thread類
- 局限性:
單繼承的局限性
任務(wù)中的成員變量不共享,加入static才能共享
1、步驟:
第一、定義類繼承Thread。
第二、復(fù)寫Thread類中的run方法。
第三、調(diào)用線程的start方法。分配并啟動該子類的實(shí)例。
start方法的作用:啟動線程,并調(diào)用run方法。
class Demo extends Thread
{
public void run()
{
for (int i=0;i<60;i++)
System.out.println(Thread.currentThread().getName() + "demo run---" + i);
}
}
class Test2
{
public static void main(String[] args)
{
Demo d1 = new Demo();//創(chuàng)建一個(gè)對象就創(chuàng)建好了一個(gè)線程
Demo d2 = new Demo();
d1.start();//開啟線程并執(zhí)行run方法
d2.start();
for (int i=0;i<60;i++)
System.out.println("Hello World!---" + i);
}
}
2、運(yùn)行特點(diǎn):
A.并發(fā)性:
我們看到的程序(或線程)并發(fā)執(zhí)行,其實(shí)是一種假象。有一點(diǎn)需要明確:;在某一時(shí)刻,只有一個(gè)程序在運(yùn)行(多核除外),此時(shí)cpu是在進(jìn)行快速的切換,以達(dá)到看上去是同時(shí)運(yùn)行的效果。由于切換時(shí)間是非常短的,所以我們可以認(rèn)為是在并發(fā)進(jìn)行。
B.隨機(jī)性:
在運(yùn)行時(shí),每次的結(jié)果不同。由于多個(gè)線程都在獲取cpu的執(zhí)行權(quán),cpu執(zhí)行到哪個(gè)線程,哪個(gè)線程就會執(zhí)行。可以將多線程運(yùn)行的行為形象的稱為互相搶奪cpu的執(zhí)行權(quán)。這就是多線程的特點(diǎn),隨機(jī)性。執(zhí)行到哪個(gè)程序并不確定。
3、覆蓋run方法的原因:
1)Thread類用于描述線程。該類定義了一個(gè)功能:用于存儲線程要運(yùn)行的代碼,該存儲功能即為run方法。也就是說,Thread類中的run方法用于存儲線程要運(yùn)行的代碼,就如同main方法存放的代碼一樣。
2)復(fù)寫run的目的:將自定義代碼存儲在run方法中,讓線程運(yùn)行要執(zhí)行的代碼。直接調(diào)用run,就是對象在調(diào)用方法。調(diào)用start(),開啟線程并執(zhí)行該線程的run方法。如果直接調(diào)用run方法,只是將線程創(chuàng)建了,但未運(yùn)行。
方式二:實(shí)現(xiàn)Runnable接口
- 局限性:
沒有返回值
任務(wù)無法拋異常給調(diào)用者
1、步驟:
第一、定義類實(shí)現(xiàn)Runnable接口。
第二、覆蓋Runnable接口中的run方法。
第三、通過Thread類建立線程對象。要運(yùn)行幾個(gè)線程,就創(chuàng)建幾個(gè)對象。
第四、將Runnable接口的子類對象作為參數(shù)傳遞給Thread類的構(gòu)造函數(shù)。
第五、調(diào)用Thread類的start方法開啟線程,并調(diào)用Runnable接口子類的run方法。
//多個(gè)窗口同時(shí)賣票
class Ticket implements Runnable
{
private int tic = 20;
public void run()
{
while(true)
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//創(chuàng)建一個(gè)線程
Thread t2 = new Thread(t);//創(chuàng)建一個(gè)線程
Thread t3 = new Thread(t);//創(chuàng)建一個(gè)線程
Thread t4 = new Thread(t);//創(chuàng)建一個(gè)線程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2、說明:
A.步驟2覆蓋run方法:將線程要運(yùn)行的代碼存放在該run方法中。
B.步驟4:為何將Runnable接口的子類對象傳給Thread構(gòu)造函數(shù)。因?yàn)樽远x的run方法所屬對象為Runnable接口的子類對象,所以讓線程指定對象的run方法,就必須明確該run方法所屬的對象。
方式三:實(shí)現(xiàn)Callable接口
利用
FutureTask
執(zhí)行任務(wù)
// 實(shí)現(xiàn)接口
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("我是實(shí)現(xiàn)Callable的任務(wù)");
return "success";
}
}
// 執(zhí)行
FutureTask<String> target = new FutureTask<>(new MyCallable());
new Thread(target).start();
log.info(target.get());
需要注意的是:局部變量在每一個(gè)線程中都獨(dú)有一份。
2、Thread類中的一些方法簡介:
在這簡單介紹幾個(gè)Thread中的方法:
1、線程名稱
- 獲取線程名稱:
getName()
每個(gè)線程都有自己默認(rèn)的名稱,
也就是說,線程一為:Thread-0,線程二為:Thread-1。
也可以獲取當(dāng)前線程對象的名稱,通過currentThread().getName()
。
// 調(diào)用
對象.getName();
// 結(jié)果
Thread-編號(從0開始)
- 設(shè)置線程名稱:
setName()
或構(gòu)造函數(shù)
可以通過setName()設(shè)置線程名稱,或者通過含有參數(shù)的構(gòu)造函數(shù)直接顯式初始化線程的名稱。如:Test(String name)
2、線程的禮讓:
- 優(yōu)先級:
setPriority()
在Thread中,存在著1~10這十個(gè)執(zhí)行級別;但是并不是優(yōu)先級越高,就會一直執(zhí)行這個(gè)線程,只是說會優(yōu)先執(zhí)行到這個(gè)線程,此后還是有其他線程會和此線程搶奪cpu執(zhí)行權(quán)的。
cpu比較忙時(shí),優(yōu)先級高的線程獲取更多的時(shí)間片
cpu比較閑時(shí),優(yōu)先級設(shè)置基本沒用
優(yōu)先級是可以設(shè)定的,可通過setPriority()設(shè)定
//最低
public final static int MIN_PRIORITY = 1;
//默認(rèn)
public final static int NORM_PRIORITY = 5;
// 最高
public final static int MAX_PRIORITY = 10;
// 方法的定義
public final void setPriority(int newPriority) {
}
yield()
此方法可暫停當(dāng)前線程,而執(zhí)行其他線程。通過這個(gè)方法,可稍微減少線程執(zhí)行頻率,達(dá)到線程都有機(jī)會平均被執(zhí)行的效果。
即讓運(yùn)行中的線程切換到就緒狀態(tài),重新爭搶cpu的時(shí)間片,爭搶時(shí)是否獲取到時(shí)間片看cpu的分配。
如下示例:t2線程每次執(zhí)行時(shí)進(jìn)行了yield(),線程1執(zhí)行的機(jī)會明顯比線程2要多。
// 方法的定義
public static native void yield();
Runnable r1 = () -> {
int count = 0;
for (;;){
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (;;){
Thread.yield();
log.info(" ---- 2>" + count++);
}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();
// 運(yùn)行結(jié)果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield - ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
3、守護(hù)線程:setDaemon()
- 默認(rèn)情況下,java進(jìn)程需要等待所有線程都運(yùn)行結(jié)束,才會結(jié)束;
有一種特殊線程叫守護(hù)線程,當(dāng)所有的非守護(hù)線程都結(jié)束后,即使它沒有執(zhí)行完,也會強(qiáng)制結(jié)束。
默認(rèn)的線程都是非守護(hù)線程。- 垃圾回收線程就是典型的守護(hù)線程
- 可將一個(gè)線程標(biāo)記為守護(hù)線程,直接調(diào)用
setDaemon()
方法。
此方法需要在啟動前調(diào)用守護(hù)線程在這個(gè)線程結(jié)束后,會自動結(jié)束,則Jvm虛擬機(jī)也結(jié)束運(yùn)行。
........
//守護(hù)線程(后臺線程),在啟動前調(diào)用。后臺線程自動結(jié)束
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
.........
4、線程的阻塞:
- 阻塞方式:
BIO阻塞,即使用了阻塞式的IO流
sleep(long time)
讓線程休眠進(jìn)入阻塞狀態(tài)
a.join()
調(diào)用該方法的線程進(jìn)入阻塞,等待a線程執(zhí)行完恢復(fù)運(yùn)行
sychronized
或ReentrantLock
造成線程未獲得鎖進(jìn)入阻塞狀態(tài) (同步鎖章節(jié)細(xì)說)
獲得鎖之后調(diào)用wait()
方法 也會讓線程進(jìn)入阻塞狀態(tài) (同步鎖章節(jié)細(xì)說)
LockSupport.park()
讓線程進(jìn)入阻塞狀態(tài) (同步鎖章節(jié)細(xì)說)sleep()
使線程休眠,會將運(yùn)行中的線程進(jìn)入阻塞狀態(tài)。當(dāng)休眠時(shí)間結(jié)束后,重新爭搶cpu的時(shí)間片繼續(xù)運(yùn)行
// 方法的定義 native方法
public static native void sleep(long millis) throws InterruptedException;
try {
// 休眠2秒
// 該方法會拋出 InterruptedException異常 即休眠過程中可被中斷,被中斷后拋出異常
Thread.sleep(2000);
} catch (InterruptedException異常 e) {
}
try {
// 使用TimeUnit的api可替代 Thread.sleep
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
join()
【臨時(shí)加入線程】
特點(diǎn):當(dāng)A線程執(zhí)行到B線程方法時(shí),A線程就會等待,B線程都執(zhí)行完,A才會執(zhí)行。join可用來臨時(shí)加入線程執(zhí)行。
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().getName() + "----run" + x);
}
}
}
class JoinDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//等t1執(zhí)行完了,主線程才從凍結(jié)狀態(tài)恢復(fù),和t2搶執(zhí)行權(quán)。t2執(zhí)不執(zhí)行完都無所謂。
int n = 0;
for(int x=0;x<80;x++){
System.out.println(Thread.currentThread().getName() + "----main" + x);
}
System.out.println("Over");
}
}
5、停止線程:
stop
【過時(shí)】
在Java1.5之后,就不再使用stop
方法停止線程了。那么該如何停止線程呢?只有一種方法,就是讓run方法結(jié)束。
開啟多線程運(yùn)行,運(yùn)行代碼通常為循環(huán)結(jié)構(gòu),只要控制住循環(huán),就可以讓run方法結(jié)束,也就可以使線程結(jié)束。
注: 特殊情況:當(dāng)線程處于凍結(jié)狀態(tài),就不會讀取標(biāo)記,那么線程就不會結(jié)束。如下:
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
public void changeFlag(){
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 n = 0;
while (true){
if (n++ == 60){
st.changeFlag();
break;
}
System.out.println("Hello World!");
}
}
}
這時(shí),當(dāng)沒有指定的方式讓凍結(jié)的線程回復(fù)打破運(yùn)行狀態(tài)時(shí),就需要對凍結(jié)進(jìn)行清除。強(qiáng)制讓線程回復(fù)到運(yùn)行狀態(tài)來,這樣就可以操作標(biāo)記讓線程結(jié)束。
interrupt()
1、此方法是為了讓線程中斷,但是并沒有結(jié)束運(yùn)行,讓線程恢復(fù)到運(yùn)行狀態(tài),再判斷標(biāo)記從而停止循環(huán),run方法結(jié)束,線程結(jié)束。
2、可以打斷sleep,wait,join等顯式的拋出InterruptedException方法的線程,但是打斷后,線程的打斷標(biāo)記還是false
isInterrupted()
獲取線程的打斷標(biāo)記 ,調(diào)用后不會修改線程的打斷標(biāo)記
interrupted()
獲取線程的打斷標(biāo)記,調(diào)用后清空打斷標(biāo)記 即如果獲取為true 調(diào)用后打斷標(biāo)記為false (不常用)
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
}
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 n = 0;
while (true){
if (n++ == 60){
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("Hello World!");
}
}
}
三、線程的運(yùn)行狀態(tài)
1、系統(tǒng)線程的狀態(tài)
- 初始狀態(tài):創(chuàng)建線程對象時(shí)的狀態(tài)
- 可運(yùn)行狀態(tài)(就緒狀態(tài)):調(diào)用start()方法后進(jìn)入就緒狀態(tài),也就是準(zhǔn)備好被cpu調(diào)度執(zhí)行
- 運(yùn)行狀態(tài):線程獲取到cpu的時(shí)間片,執(zhí)行run()方法的邏輯
- 阻塞狀態(tài): 線程被阻塞,放棄cpu的時(shí)間片,等待解除阻塞重新回到就緒狀態(tài)爭搶時(shí)間片
- 終止?fàn)顟B(tài): 線程執(zhí)行完成或拋出異常后的狀態(tài)
需要說明的是:
- 阻塞狀態(tài):具備運(yùn)行資格,但是沒有執(zhí)行權(quán),必須等到cpu的執(zhí)行權(quán),才轉(zhuǎn)到運(yùn)行狀態(tài)。
- 凍結(jié)狀態(tài):放棄了cpu的執(zhí)行資格,cpu不會將執(zhí)行權(quán)分配給這個(gè)狀態(tài)下的線程,必須被喚醒后,此線程要先轉(zhuǎn)換到阻塞狀態(tài),等待cpu的執(zhí)行權(quán)后,才有機(jī)會被執(zhí)行到。
2、Thread類定義的線程狀態(tài)
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
-
NEW
線程對象被創(chuàng)建 -
RUNNALE
線程調(diào)用了start()方法后進(jìn)入該狀態(tài),該狀態(tài)包含了三種情況- 就緒狀態(tài) :等待cpu分配時(shí)間片
- 運(yùn)行狀態(tài):進(jìn)入Runnable方法執(zhí)行任務(wù)
- 阻塞狀態(tài):BIO 執(zhí)行阻塞式io流時(shí)的狀態(tài)
-
BLOCKED
沒獲取到鎖時(shí)的阻塞狀態(tài)(同步鎖章節(jié)會細(xì)說) -
WAITING
調(diào)用wait()、join()等方法后的狀態(tài) -
TIMED_WAITING
調(diào)用 sleep(time)、wait(time)、join(time)等方法后的狀態(tài) -
TERMINATED
線程執(zhí)行完成或拋出異常后的狀態(tài)
四、上下文切換
CPU在多個(gè)線程間進(jìn)行調(diào)度,運(yùn)行時(shí)會進(jìn)行上下文切換
發(fā)生切換場景:
- 線程的cpu時(shí)間片用完
- 垃圾回收
- 線程自己調(diào)用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
當(dāng)發(fā)生上下文切換時(shí),操作系統(tǒng)會保存當(dāng)前線程的狀態(tài),并恢復(fù)另一個(gè)線程的狀態(tài),jvm中有塊內(nèi)存地址叫程序計(jì)數(shù)器,用于記錄線程執(zhí)行到哪一行代碼,是線程私有的。
五、多線程的安全問題:
在那個(gè)簡單的賣票小程序中,發(fā)現(xiàn)打印出了0、-1、-2等錯(cuò)票,也就是說這樣的多線程在運(yùn)行的時(shí)候是存在一定的安全問題的。
1、為什么會出現(xiàn)這種安全問題呢?
原因是當(dāng)多條語句在操作同一線程共享數(shù)據(jù)時(shí),一個(gè)線程對多條語句只執(zhí)行了一部分,還未執(zhí)行完,另一線程就參與進(jìn)來執(zhí)行了,導(dǎo)致共享數(shù)據(jù)發(fā)生錯(cuò)誤。
以也就是說,由于cpu的快速切換,當(dāng)執(zhí)行線程一時(shí),tic為1了,執(zhí)行到if (tic > 0)的時(shí)候,cpu就可能將執(zhí)行權(quán)給了線程二,那么線程一就停在這條語句了,tic還沒減1,仍為1;線程二也判斷if (tic > 0)是符合的,也停在這,以此類推。
當(dāng)cpu再次執(zhí)行線程一的時(shí)候,打印的是1號,執(zhí)行線程二的時(shí)候,是2號票,以此類推,就出現(xiàn)了錯(cuò)票的結(jié)果。其實(shí)就是多條語句被共享了,如果是一條語句,是不會出現(xiàn)此種情況的。
-
問題根源
:一行代碼編譯成字節(jié)碼的時(shí)候可能為多行,在多個(gè)線程上下文切換時(shí)就可能交錯(cuò)執(zhí)行。
2、線程安全
-
線程安全
:多線程調(diào)用同一個(gè)對象的臨界區(qū)的方法時(shí),對象的屬性值一定不會發(fā)生錯(cuò)誤,這就保證了線程安全。
線程安全的類一定所有的操作都線程安全嗎?
開發(fā)中經(jīng)常會說到一些線程安全的類,如ConcurrentHashMap,線程安全指的是類里每一個(gè)獨(dú)立的方法是線程安全的,但是方法的組合就不一定是線程安全的。
成員變量和靜態(tài)變量是否線程安全?
- 如果沒有多線程共享,則線程安全
- 如果存在多線程共享
- 多線程只有讀操作,則線程安全
- 多線程存在寫操作,寫操作的代碼又是臨界區(qū),則線程不安全
局部變量是否線程安全?
- 局部變量是線程安全的
- 局部變量引用的對象未必是線程安全的
- 如果該對象沒有逃離該方法的作用范圍,則線程安全
- 如果該對象逃離了該方法的作用范圍,比如:方法的返回值,需要考慮線程安全
3、那么該如何解決呢?(synchronized
)
對于多條操作共享數(shù)據(jù)的語句,只能讓一個(gè)線程都執(zhí)行完,在執(zhí)行過程中,其他線程不可參與執(zhí)行,就不會出現(xiàn)問題了。Java對于多線程的安全問題,提供了專業(yè)的解決方式,即同步代碼塊,可操作共享數(shù)據(jù)。
1、同步代碼塊
synchronized(對象)//對象稱為鎖旗標(biāo)
{
需要被同步的代碼
}
其中的對象如同鎖,持有鎖的線程可在同步中執(zhí)行,沒有鎖的線程,即使獲得cpu的執(zhí)行權(quán),也進(jìn)不去,因?yàn)闆]有獲取鎖,是進(jìn)不去代碼塊中執(zhí)行共享數(shù)據(jù)語句的。
同步的前提:
- A.必須有兩個(gè)或兩個(gè)以上的線程
- B.必須保證同步的線程使用同一個(gè)鎖。必須保證同步中只能有一個(gè)線程在運(yùn)行。
好處與弊端:
- 解決了多線程的安全問題(阻塞式的解決方案)。
- 多個(gè)線程需要判斷鎖,較為消耗資源。
class Ticket implements Runnable
{
private int tic = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)//任意的一個(gè)對象
{
//此兩句為共享語句
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t,"1");//創(chuàng)建第一個(gè)線程
Thread t2 = new Thread(t,"2");//創(chuàng)建第二個(gè)線程
//開啟線程
t1.start();
t2.start();
}
}
2、同步函數(shù)
同步函數(shù)就是將修飾符synchronized放在返回類型的前面,下面通過同步函數(shù)給出多線程安全問題的具體解決方案:
1)目的:判斷程序中是否有安全問題,若有,該如何解決。
2)解決:
第一、明確哪些代碼是多線程的運(yùn)行代碼
第二、明確共享數(shù)據(jù)
第三、明確多線程運(yùn)行代碼中,哪些語句是操作共享數(shù)據(jù)的。
示例:
class Bank
{
private int sum;//共享數(shù)據(jù)
//run中調(diào)用了add,所以其也為多線程運(yùn)行代碼
public synchronized void add(int n)//同步函數(shù),用synchronized修飾
{
//這有兩句操作,是操作共享數(shù)據(jù)的
sum += n;
System.out.println("sum" + sum);
}
}
class Cus implements Runnable
{
private Bank b = new Bank();//共享數(shù)據(jù)
//多線程運(yùn)行代碼run
public void run()
{
for (int i=0;i<3;i++)
{
b.add(100);//一句,不會分開執(zhí)行,所以沒問題
}
}
}
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();
}
}
六、同步函數(shù)中的鎖:
1、非靜態(tài)同步函數(shù)中的鎖:this
函數(shù)需被對象調(diào)用,那么函數(shù)都有一個(gè)所屬的對象引用,就是this,因此同步函數(shù)使用的鎖為this。
測驗(yàn)如下:
class Ticket implements Runnable
{
private int tic = 100;
boolean flog = true;
public void run()
{
if (flog)
{
//線程一執(zhí)行
while(true)
{
//如果對象為obj,則是兩個(gè)鎖,是不安全的;換成this,為一個(gè)鎖,會安全很多
synchronized(this)
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);
}
}
}
//線程二執(zhí)行
else
while(true)
show();
}
public synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}
class ThisLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//創(chuàng)建一個(gè)線程
Thread t2 = new Thread(t);//創(chuàng)建一個(gè)線程
t1.start();
t.flog = false;//開啟線程一,即關(guān)閉if,讓線程二執(zhí)行else中語句
t2.start();
}
}
讓線程一執(zhí)行打印cobe的語句,讓線程二執(zhí)行打印show的語句。如果對象換位另一個(gè)對象obj,那將是兩個(gè)鎖,因?yàn)樵谥骱瘮?shù)中創(chuàng)建了一個(gè)對象即Ticket t = new Ticket();,線程會共享這個(gè)對象調(diào)用的run方法中的數(shù)據(jù),所以都是這個(gè)t對象在調(diào)用,那么,其中的對象應(yīng)為this;否則就破壞了同步的前提,就會出現(xiàn)安全問題。
2、靜態(tài)同步函數(shù)中的鎖:
如果同步函數(shù)被靜態(tài)修飾后,經(jīng)驗(yàn)證,使用的鎖不是this了,因?yàn)殪o態(tài)方法中不可定義this,所以,這個(gè)鎖不再是this了。靜態(tài)進(jìn)內(nèi)存時(shí),內(nèi)存中沒有本類對象,但是一定有該類對應(yīng)的字節(jié)碼文件對象:類名.class;該對象的類型是Class。
所以靜態(tài)的同步方法使用的鎖是該方法所在類的字節(jié)碼文件對象,即類名.class。
示例:
>class Ticket implements Runnable
{
//私有變量,共享數(shù)據(jù)
private static int tic = 100;
boolean flog = true;
public void run()
{
//線程一執(zhí)行
if (flog)
{
while(true)
{
synchronized(Ticket.class)//不再是this了,是Ticket.class
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);
}
}
}
//線程二執(zhí)行
else
while(true)
show();
}
public static synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}
class StaticLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//創(chuàng)建第一個(gè)線程
Thread t2 = new Thread(t);//創(chuàng)建第二個(gè)線程
t1.start();
t.flog = false;
t2.start();
}
}
在之前,也提到過關(guān)于多線程的安全問題的相關(guān)知識,就是在單例設(shè)計(jì)模式中的懶漢式中,用到了鎖的機(jī)制。
七、多線程間的通信:
多線程間通信是線程之間進(jìn)行交互的方式,簡單說就是存儲資源 和 獲取資源。比如說倉庫中的貨物,有進(jìn)貨的,有出貨的。還比如生產(chǎn)者和消費(fèi)者的例子。這些都可以作為線程通信的實(shí)例。那么如何更好地實(shí)現(xiàn)通信呢?
先看下面的代碼:
/*
線程間通信:
等待喚醒機(jī)制:升級版
生產(chǎn)者消費(fèi)者 多個(gè)
*/
import java.util.concurrent.locks.*;
class ProducerConsumerDemo{
public static void main(String[] args){
Resouse r = new Resouse();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resouse{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock();
private Condition condition_P = lock.newCondition();
private Condition condition_C = lock.newCondition();
//要喚醒全部,否則都可能處于凍結(jié)狀態(tài),那么程序就會停止。這和死鎖有區(qū)別的。
public void set(String name)throws InterruptedException{
lock.lock();
try{
while(flag)//循環(huán)判斷,防止都凍結(jié)狀態(tài)
condition_P.await();
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);
flag = true;
condition_C.signal();
}finally{
lock.unlock();//釋放鎖的機(jī)制一定要執(zhí)行
}
}
public void out()throws InterruptedException{
lock.lock();
try{
while(!flag)//循環(huán)判斷,防止都凍結(jié)狀態(tài)
condition_C.await();
System.out.println(Thread.currentThread().getName() + "..消費(fèi)者." + this.name);
flag = false;
condition_P.signal();//喚醒全部
}finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resouse r;
Producer(Resouse r){
this.r = r;
}
public void run(){
while(true){
try{
r.set("--商品--");
}catch (InterruptedException e){}
}
}
}
class Consumer implements Runnable{
private Resouse r;
Consumer(Resouse r){
this.r = r;
}
public void run(){
while(true){
try{
r.out();
}catch (InterruptedException e){}
}
}
}
1、等待喚醒機(jī)制:
1、顯式鎖機(jī)制和等待喚醒機(jī)制:
在JDK 1.5中,提供了改進(jìn)synchronized的升級解決方案。將同步synchronized替換為顯式的Lock操作,將Object中的wait,notify,notifyAll替換成Condition對象,該對象可對Lock鎖進(jìn)行獲取。這就實(shí)現(xiàn)了本方喚醒對方的操作。
在這里說明幾點(diǎn):
1)、對于wait,notify和notifyAll這些方法都是用在同步中,也就是等待喚醒機(jī)制,這是因?yàn)橐獙Τ钟斜O(jiān)視器(鎖)的線程操作。所以要使用在同步中,因?yàn)橹挥型讲啪哂墟i。
2)、而這些方法都定義在Object中,是因?yàn)檫@些方法操作同步中的線程時(shí),都必須表示自己所操作的線程的鎖,就是說,等待和喚醒的必須是同一把鎖。不可對不同鎖中的線程進(jìn)行喚醒。所以這就使得程序是不良的,因此,通過對鎖機(jī)制的改良,使得程序得到優(yōu)化。
3)、等待喚醒機(jī)制中,等待的線程處于凍結(jié)狀態(tài),是被放在線程池中,線程池中的線程已經(jīng)放棄了執(zhí)行資格,需要被喚醒后,才有被執(zhí)行的資格。
2、對于上面的程序,有兩點(diǎn)要說明:
1)、為何定義while判斷標(biāo)記:
原因是讓被喚醒的線程再判斷一次。
避免未經(jīng)判斷,線程不知是否應(yīng)該執(zhí)行,就執(zhí)行本方的上一個(gè)已經(jīng)執(zhí)行的語句。如果用if,消費(fèi)者在等著,兩個(gè)生成著一起判斷完flag后,cpu切換到其中一個(gè)如t1,另一個(gè)t3在wait,當(dāng)t1喚醒凍結(jié)中的一個(gè),是t3(因?yàn)樗缺粌鼋Y(jié)的,就會先被喚醒),所以t3未經(jīng)判斷,又生產(chǎn)了一個(gè)。而沒消費(fèi)。
2)這里使用的是signal方法,而不是signalAll方法。是因?yàn)橥ㄟ^Condition的兩個(gè)對象,分別喚醒對方,這就體現(xiàn)了Lock鎖機(jī)制的靈活性。可以通過Contidition對象調(diào)用Lock接口中的方法,就可以保證多線程間通信的流暢性了。
對于多線程的知識,還需要慢慢積累,畢竟線程通信可以提高程序運(yùn)行的效率,這樣就可以讓程序得到很大的優(yōu)化。期待新知識······
八、線程池
1、簡述:
預(yù)先創(chuàng)建好一些線程,任務(wù)提交時(shí)直接執(zhí)行,既可以節(jié)約創(chuàng)建線程的時(shí)間,又可以控制線程的數(shù)量。
2、線程池的好處
- 降低資源消耗,通過池化思想,減少創(chuàng)建線程和銷毀線程的消耗,控制資源
- 提高響應(yīng)速度,任務(wù)到達(dá)時(shí),無需創(chuàng)建線程即可運(yùn)行
- 提供更多更強(qiáng)大的功能,可擴(kuò)展性高
3、線程池的主要流程
流程包括:線程池創(chuàng)建、接收任務(wù)、執(zhí)行任務(wù)、回收線程的步驟
線程池的構(gòu)造函數(shù):
public ThreadPoolExecutor(int corePoolSize, //核心線程數(shù)
int maximumPoolSize, //最大線程數(shù)
long keepAliveTime, //救急線程的空閑時(shí)間
TimeUnit unit, //救急線程的空閑時(shí)間單位
BlockingQueue<Runnable> workQueue, //阻塞隊(duì)列
ThreadFactory threadFactory, //創(chuàng)建線程的工廠,主要定義線程名
RejectedExecutionHandler handler //拒絕策略
) {
//......
}
- 流程:
1、創(chuàng)建線程池后,線程池的狀態(tài)是RUNNABLE,該狀態(tài)下才能有下面的步驟
2、提交任務(wù)時(shí),線程池會創(chuàng)建線程去處理任務(wù)
3、當(dāng)線程池的工作線程數(shù)達(dá)到corePoolSize時(shí),繼續(xù)提交任務(wù)會進(jìn)入阻塞隊(duì)列
4、當(dāng)阻塞隊(duì)列裝滿時(shí),繼續(xù)提交任務(wù),會創(chuàng)建救急線程來處理
5、當(dāng)線程池中的工作線程數(shù)達(dá)到maximumPoolSize時(shí),會執(zhí)行拒絕策略
6、當(dāng)線程取任務(wù)的時(shí)間達(dá)到keepAliveTime還沒有取到任務(wù),工作線程數(shù)大于corePoolSize時(shí),會回收該線程
- 注意: 不是剛創(chuàng)建的線程是核心線程,后面創(chuàng)建的線程是非核心線程;線程是沒有核心非核心的概念的。
- 拒絕策略
1、調(diào)用者拋出RejectedExecutionException (默認(rèn)策略)
2、讓調(diào)用者運(yùn)行任務(wù)
3、丟棄此次任務(wù)
4、丟棄阻塞隊(duì)列中最早的任務(wù),加入該任務(wù)
- 提交任務(wù)的方法
// 執(zhí)行Runnable
public void execute(Runnable command) {
}
// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 內(nèi)部構(gòu)建FutureTask
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 內(nèi)部構(gòu)建FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 內(nèi)部構(gòu)建FutureTask
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
4、Execetors創(chuàng)建線程池
-
newFixedThreadPool
(定長線程池)
核心線程數(shù) = 最大線程數(shù) 沒有救急線程
阻塞隊(duì)列無界 可能導(dǎo)致oom
可控制線程最大并發(fā)數(shù),超出的線程會在隊(duì)列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
newCachedThreadPool
(可緩存線程池)
核心線程數(shù)是0,最大線程數(shù)無限制 ,救急線程60秒回收
隊(duì)列采用 SynchronousQueue 實(shí)現(xiàn),沒有容量,即放入隊(duì)列后沒有線程來取就放不進(jìn)去
可能導(dǎo)致線程數(shù)過多,cpu負(fù)擔(dān)太大
如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
newSingleThreadExecutor
(單線程化的線程池)
核心線程數(shù)和最大線程數(shù)都是1,沒有救急線程,無界隊(duì)列 可以不停的接收任務(wù)
將任務(wù)串行化 一個(gè)個(gè)執(zhí)行, 使用包裝類是為了屏蔽修改線程池的一些參數(shù) 比如 corePoolSize
如果某線程拋出異常了,會重新創(chuàng)建一個(gè)線程繼續(xù)執(zhí)行
可能造成oom
用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行
現(xiàn)行大多數(shù)GUI程序都是單線程的。Android中單線程可用于數(shù)據(jù)庫操作,文件操作,應(yīng)用批量安裝,應(yīng)用批量刪除等不適合并發(fā)但可能IO阻塞性及影響UI線程響應(yīng)的操作。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
-
newScheduledThreadPool
(定長線程池)
任務(wù)調(diào)度的線程池。可以指定延遲時(shí)間調(diào)用,可以指定隔一段時(shí)間調(diào)用
支持定時(shí)及周期性任務(wù)執(zhí)行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
5、線程池的關(guān)閉
shutdown()
會讓線程池狀態(tài)為shutdown,不能接收任務(wù),但是會將工作線程和阻塞隊(duì)列里的任務(wù)執(zhí)行完 相當(dāng)于優(yōu)雅關(guān)閉shutdownNow()
會讓線程池狀態(tài)為stop, 不能接收任務(wù),會立即中斷執(zhí)行中的工作線程,并且不會執(zhí)行阻塞隊(duì)列里的任務(wù), 會返回阻塞隊(duì)列的任務(wù)列表
6、線程池的使用
- 配置參數(shù):
- cpu密集型 : 指的是程序主要發(fā)生cpu的運(yùn)算
核心線程數(shù) = CPU核心數(shù)+1- IO密集型:遠(yuǎn)程調(diào)用RPC,操作數(shù)據(jù)庫等,不需要使用cpu進(jìn)行大量的運(yùn)算。 大多數(shù)應(yīng)用的場景
核心線程數(shù) = 核數(shù)cpu期望利用率總時(shí)間 / cpu運(yùn)算時(shí)間