Java 多線程基礎
1.多線程基礎
1)進程
進程(process)就是一塊包含了某些資源的內存區域。操作系統利用進程把它的工作劃分為一些功能單元。進程中所包含的一個或多個執行單元成為線程(thread)。進程還擁有一個私有的虛擬地址空間,該空間僅能被它所包含的線程訪問。線程只能歸屬於一個進程並且它只能訪問該進程所擁有的資源。當操作系統創建一個進程後,該進程會自動申請一個名為主線程或者首要線程的線程。操作系統中有若幹個線程再“同時”運行。通常,操作系統上運行的每一個應用程序都運行在一個進程中,例如QQ、IE等。
進程並不是真正意義上的同時運行,而是並發運行。
2)線程
一個線程是進程的一個順序執行流。同類的多個線程共享一塊內存空間和一組系統資源,線程本身有一個供程序執行的堆棧。線程再切換時負荷小,因此線程也被成為輕負荷進程,一個進程中可以包含多個線程。
切換:線程並發時的一種現象。
3)進程與線程的區別
一個進程至少有一個線程,線程的劃分尺度小於進程,使得多線程程序的並發性高。另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大的提高了程序的運行效率。
線程在執行過程中與進程的區別在於每個獨立的線程有一個程序運行的入口,順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用來實現進程的調度和管理以及資源分配。
4)線程使用場合
線程通常用於在一個程序中需要同時完成多個任務的情況,我們可以將每個任務定義為一個線程,使他們得以一同工作。
例如我們在玩某個遊戲時,這個遊戲由操作系統運行,所以其運行在一個獨立的進程中,而在遊戲中我們會聽到某些背景音樂、某個角色在移動、出現某些絢麗的動畫效果等,這些在遊戲中都是同時發生的,但實際上,播放音樂是在一個線程中獨立完成的,移動某個角色,播放某些特效也都是在獨立的線程中完成的,這些事情我們無法在單一線程中完成。
也可以用於在單一線程中可以完成,但是使用多線程可以更快的情況,比如下載文件,如迅雷,通常會打開多個節點來同時下載一個文件。
5)並發原理
多個線程或進程“同時”運行只是我們感官上的一種表現,事實上進程和線程是並發運行的。OS的線程調度機制將時間劃分為很多時間片段(時間片),盡可能均勻分配給正在運行的程序,獲取CPU時間片的線程或進程得以被執行,其他則等待。而CPU則在這些進程或線程上來回切換運行,圍觀上所有進程和線程是走走停停的,宏觀上都在運行,這種都運行的現象叫並發,但不是絕對意義上的“同時發生”。
之所以這樣做是因為CPU只有一個,同一時間只能做一件事,但隨著計算機的發展,出現了多核心CPU,例如兩核心CPU可以實現真正意義上的2線程同時運行,但因為CPU的時間片段分配給哪個進程或線程是由線程調度決定,所以不一定兩個線程都是屬於同一個進程的,無論如何我們只需要知道線程或進程是並發運行即可。
OS:Operating System 操作系統。
線程調度機制是OS提供的一個用於並發處理的程序,Java虛擬機自身也提供了線程調度機制,用於減輕OS切換線程帶來的更多負擔。
6)線程狀態
對於程序而言,我們實際上關心的是線程而非進程,線程在其生命周期中的各個狀態如下:
- New:當我們創建一個線程時,該線程並沒有納入線程調度,其處於一個new狀態。
- Runnable:當調用線程的start方法後,該線程納入線程調度的控制,其處於一個可運行狀態,等地分配時間片以並發運行。
- Running:當線程被分配到了時間片段後其被CPU運行,這時該線程處於running狀態。
- Blocked:當線程在運行過程中可能會出現阻塞現象,比如等待用戶輸入信息等,但堵塞狀態不是百分百出現的,具體要看代碼中是否有相關需求。
- Dead:當線程的任務全部運行完畢,或在運行過程中拋出了一個未捕獲的異常,那麽線程結束,等待GC回收。
2.創建線程
1)使用Thread創建線程並啟動線程
Thread類是線程類,其每一個實例表示一個可以並發運行的線程。我們可以通過繼承該類並重寫run方法來定義一個具體的線程。其中重寫run方法的目的是定義該線程要執行的邏輯。啟動線程時調用線程的start()方法而非直接調用run()方法。start()方法會將當前線程納入線程調度,使當前線程可以開始並發運行。當線程獲取時間片段後會自動開始執行run方法中的邏輯。
案例1:創建線程方法一:繼承線程Thread並重寫run
public class Thread_currentThread {
public static void main(String[] args) {
Thread t1 = new Mythread1(); //線程實例化
Thread t2 = new Mythread2();
t1.start();
t2.start();
}
}
class Mythread1 extends Thread{ //創建方法繼承Thread
public void run(){ //重寫run方法
for(int i=0;i<1000;i++){
System.out.println("線程1");
}
}
}
class Mythread2 extends Thread{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("線程2");
}
}
}
2)使用Runnable創建並啟動線程
實現Runnable接口並重寫run方法來定義線程體,然後在創建線程的時候將Runnable的實例傳入並啟動線程。
這樣做的好處在於可以將線程與線程要執行的任務分離開減少耦合,同時Java是單繼承的,定義一個類實現Runnable接口這樣的做法可以更好的去實現其他父類或接口,因為接口是多繼承的。
案例2:創建線程方法二:實現Runnable接口單獨定義線程任務
public class RunnableDemo {
public static void main(String[] args) {
Runnable run1 = new MyRunnable1();
Runnable run2 = new MyRunnable2();
Thread t1 = new Thread(run1);
Thread t2 = new Thread(run2);
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("線程1");
}
}
}
class MyRunnable2 implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("線程2");
}
}
}
3)使用內部類創建線程
通常我們可以通過匿名內部類的方式創建線程,使用該方式可以簡化編寫代碼的復雜度,當一個線程僅需要一個實例時我們通常使用這種方式來創建。
//用方式一和方式二,匿名內部類的方式創建兩個線程
public class ThreadDemo {
public static void main(String[] args) {
//方式一:繼承Thread,重寫run
Thread t1 = new Thread(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println("線程1");
}
}
};
//方式二:Runnable方式
Runnable r2 = new Runnable(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println("線程2");
}
}
};
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
3.線程操作API
1)Thread.currentThread方法
Thread的靜態方法currentThread方法可以用於獲取運行當前代碼的線程。
/*
* 線程提供了一個靜態方法:
* static Thread currentThread()
* 該方法可以獲取運行當前方法的線程
*/
public class ThreadcurrentThreadDemo {
public static void main(String[] args) {
//獲取main方法的線程
Thread main = Thread.currentThread();
System.out.println("運行main方法的線程是:"+main);
Thread t = new Thread(){
public void run(){
Thread t = Thread.currentThread();
System.out.println("自定義線程是:"+t);
ct();
}
};
ct();
t.start();
}
public static void ct(){
Thread t = Thread.currentThread();
System.out.println("運行ct方法的線程是"+t);
}
}
/*
運行結果:
運行main方法的線程是:Thread[main,5,main]
運行ct方法的線程是Thread[main,5,main]
自定義線程是:Thread[Thread-0,5,main]
運行ct方法的線程是Thread[Thread-0,5,main]
*/
2)獲取線程信息
Thread提供了獲取線程信息的相關方法:
- long getId():返回該線程的標識符
- String getName():返回該線程的名稱
- int getPriority():返回線程的優先級
- Thread.state getState():獲取線程的狀態
- boolean isAlive():測試線程是否處於活動狀態
- boolean isDaemon():測試線程是否為守護線程
- boolean isInterrupted():測試線程是否已經中斷
public class Thread_info {
public static void main(String[] args) {
//獲取main方法的線程
Thread t = Thread.currentThread();
long id = t.getId();
System.out.println("id:"+id);
String name = t.getName();
System.out.println("name:"+name);
int priority = t.getPriority();
System.out.println("優先級:"+priority);
boolean isAlive = t.isAlive();
System.out.println("isAlive"+isAlive);
boolean isDaemon = t.isDaemon();
System.out.println("isDaemon:"+isDaemon);
boolean isInterrupted = t.isInterrupted();
System.out.println("isInterrupted:"+isInterrupted);
}
}
/*
運行結果:
id:1
name:main
優先級:5
isAlivetrue
isDaemon:false
isInterrupted:false
*/
3)線程優先級
線程的切換是由線程調度控制的,我們無法通過代碼來幹涉,但是我們可以通過提高線程的優先級來最大程度的改善線程獲取時間片的幾率。
線程的優先級被分為10級,值為1-10,10最高。線程提供了3個常量表示最低,最高以及默認狀態。
- Thread.MIN_PRIORITY
- Thread.MAX_PRIORITY
- Thread.NORM_PRIORITY
設置優先級方法:
void setPriority(int priority)
public class Thread_setPriority {
public static void main(String[] args) {
Thread max = new Thread(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println("max");
}
}
};
Thread norm = new Thread(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("nor");
}
}
};
Thread min = new Thread(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("min");
}
}
};
max.setPriority(Thread.MAX_PRIORITY);
min.setPriority(Thread.MIN_PRIORITY);
min.start();
norm.start();
max.start();
}
}
4)守護線程
守護線程與普通線程在表現上沒有什麽卻別,我們只需要通過Thread提供的方法來設定即可:
void setDaemon(boolean)
當參數為true時該線程為守護線程,守護線程的特點是:當進程中只剩下守護線程時,所有守護線程強制終止。
GC就是運行在一個守護線程上的。
需要註意點是:設置線程為後臺線程要在該線程啟動前設置。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
deamonThread.start();
案例3:創建守護進程
public class Thread_setDaemon {
public static void main(String[] args) {
Thread rose = new Thread(){
public void run(){
for(int i=0;i<3;i++){
System.out.println("rose:let me jump");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("音效:噗通");
}
};
Thread jack = new Thread(){
public void run(){
System.out.println("jack:you jump I jump");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
rose.start();
jack.setDaemon(true); //要在start之前設置為後臺進程
jack.start();
}
}
5)sleep方法
Thread的靜態方法sleep用於使當前線程進入堵塞狀態:
static void sleep(long ms)
該方法會使當前線程進入堵塞狀態指定毫秒,當指定毫秒堵塞後,當前線程會重新進入Runnable狀態,等待分配時間片。該方法聲明拋出一個InterrupException,所以在使用該方法時需要捕獲這個異常。
案例4:電子鐘程序
要求:每1s中在控制臺顯示當前時間
import java.text.SimpleDateFormat;
import java.util.Date;
public class Thread_sleep {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
while(true){
Date date = new Date();
System.out.println(sdf.format(date));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
6)join方法
join語法:
void join()
該方法用於等待當前線程結束。此方法是一個阻塞方法。該方法聲明拋出InterruptException。
案例5:join()方法演示
/*
* 線程提供了join()方法,該方法允許多線程之間同步執行
*
* 同步:有先後順序的執行代碼
* 異步:各幹各的
*
* 演示:下載圖片,圖片下載完畢之後顯示圖片
*/
public class Thread_join {
public static boolean isFinish; //表示圖片是否下載完畢
public static void main(String[] args){
final Thread download = new Thread(){
public void run(){
System.out.println("down:開始下載圖片....");
for(int i=10;i<=100;i+=10){ //模擬圖片下載百分比
System.out.println("down:"+i+"%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("down:下載圖片完畢!");
isFinish = true;
}
};
Thread show = new Thread(){
public void run(){
System.out.println("show:開始顯示圖片...");
//首先等待圖片下載完成
try {
download.join(); //show線程開始堵塞,知道download線程的run方法執行完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!isFinish){
throw new RuntimeException("show:圖片沒有下載完畢!");
}
System.out.println("show:圖片顯示完畢!");
}
};
download.start();
show.start();
}
}
/*
運行結果:
show:開始顯示圖片...
down:開始下載圖片....
down:10%
down:20%
down:30%
down:40%
down:50%
down:60%
down:70%
down:80%
down:90%
down:100%
down:下載圖片完畢!
show:圖片顯示完畢!
*/
7)yield方法
Thread的靜態方法yield語法:
static void yield()
該方法用於使當前線程主動讓出當次CPU時間片回到Runnable狀態,等待分配時間片。
4.線程同步
1)synchronized關鍵字
多個線程並發讀取同一個臨界資源時會發生“線程並發安全問題”。
常見的臨界資源:
- 多線程共享實例變量
- 多線程共享靜態公共變量
若想解決線程安全問題,需要將異步的操作變為同步操作。
- 異步:多線程並發的操作,相當於各幹各的。
- 同步:有先後順序的操作,相當於你幹完我再幹。
Java中關鍵字:synchronized是同步鎖,用於將某段代碼變為同步操作,從而解決線程並發安全問題。
案例6:模擬線程並發安全問題
/*
* 多線程並發安全問題:
* 當多個線程並發訪問同一數據時,由於線程切換時機的不正確,
* 導致代碼未按照代碼設計順序執行而出現混了,嚴重時可能導致系統癱瘓。
*
* 假設桌上有20個豆子,兩個線程分別模擬兩個人去抓取豆子,這樣在最後一顆
* 豆子時,如果線程1取走了而正好在豆子歸零的時候分配的時間片到時,發生線程
* 切換進入Runnable狀態等待重新分配時間片,而此時線程2進入,發現還有一個豆子,
* 就取走了,這樣就造成了問題,最後一個豆子被取走了兩次。
* 模擬上述情況,有一定幾率發生死循環,就是最後一個豆子被取走兩次,豆子數量為負數的時候。
*/
public class Thread_SyncDemo01 {
public static void main(String[] args) {
final Table table = new Table();
Thread t1 = new Thread(){
public void run(){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
//桌上有20個豆子
private int beans = 20;
public int getBean(){ //取豆子方法
if(beans==0){ //當剩下0個豆子時拋出異常
throw new RuntimeException("沒有豆子了");
}
//模擬線程執行到這裏發生了切換
Thread.yield();
return beans--; ///每取走一顆豆子beans減1
}
}
/*
運行結果:
異常狀態:
Thread-1:-656273
Thread-1:-656274
Thread-1:-656275
Thread-1:-656276
Thread-1:-656277
Thread-1:-656278
Thread-1:-656279
Thread-1:-656280
Thread-1:-656281
正常狀態:
Thread-0:20
Thread-1:19
Thread-1:18
Thread-1:17
Thread-1:16
Thread-1:15
Thread-1:14
Thread-1:13
Thread-1:12
Thread-1:11
Thread-1:10
Thread-1:9
Thread-1:8
Thread-1:7
Thread-1:6
Thread-1:5
Thread-1:4
Thread-1:3
Thread-1:2
Thread-1:1
Exception in thread "Thread-1" java.lang.RuntimeException: 沒有豆子了
at javase.day09.Table.getBean(Thread_SyncDemo01.java:41)
at javase.day09.Thread_SyncDemo01$2.run(Thread_SyncDemo01.java:26)
*/
2)鎖機制
Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(synchronized關鍵字),同步代碼塊包含兩部分:
- 一個作為鎖的對象的引用
- 一個作為由這個鎖保護的代碼塊
sysnchronized(同步監視器——鎖對象引用){
代碼塊;
}
若方法所有代碼都需要同步也可以給方法直接枷鎖。
每個Java對象都可以用做一個實現同步的鎖,線程進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時釋放鎖,而且無論是通過正常路徑退出還是通過拋異常退出都一樣,獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
3)選擇合適的鎖對象
使用synchronized需要一個鎖對象上鎖以保護線程同步。
那麽這個鎖對象應當註意:多個需要同步的線程在訪問該同步塊時,看到的應該是同一個鎖對象引用。否則達不到同步效果,通常會使用this來作為鎖對象。
4)選擇合適的鎖範圍
在使用同步塊時,應當盡量在允許的情況下減少同步範圍,以提高並發的執行效率。
5)靜態方法鎖
public synchronized static void dosome(){
...
}
當對一個靜態方法枷鎖,那麽該方法鎖的對象是類對象,每個類都有唯一的一個類對象,獲取類對象的方式:類名.class。靜態方法與非靜態方法同時聲明了synchronized,他們之間是非互斥關系的。因為靜態方法鎖的是類對象而非靜態方法鎖的當前方法所屬對象。
案例7:synchronized修飾(同步)方法演示
public class Thread_SyncDemo01 {
public static void main(String[] args) {
final Table table = new Table();
Thread t1 = new Thread(){
public void run(){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
private int beans = 20;
/*
* 當一個方法被synchronized修飾後,那麽該方法成為“同步方法”
* 即:多個線程不能同時進入方法內部執行
* 在方法上直接使用synchronized,那麽同步監視器對象就是當前
* 方法所屬對象,即方法內部看到的“this”
*/
public synchronized int getBean(){ //使用synchronized修飾方法
if(beans==0){ //當剩下0個豆子時拋出異常
throw new RuntimeException("沒有豆子了");
}
//模擬線程執行到這裏發生了切換
Thread.yield();
return beans--;
}
}
/*
運行結果:
Thread-0:20
Thread-1:19
Thread-1:18
Thread-1:17
Thread-1:16
Thread-1:15
Thread-1:14
Thread-1:13
Thread-1:12
Thread-1:11
Thread-1:10
Thread-1:9
Thread-1:8
Thread-1:7
Thread-1:6
Thread-1:5
Thread-1:4
Thread-1:3
Thread-1:2
Thread-1:1
Exception in thread "Thread-1" java.lang.RuntimeException: 沒有豆子了
at javase.day09.Table.getBean(Thread_SyncDemo01.java:46)
at javase.day09.Thread_SyncDemo01$2.run(Thread_SyncDemo01.java:26)
*/
案例8:synchronized同步代碼塊
/*
* 模擬買衣服的情況,很多人可以進去選衣服,但是試衣服時,當試衣間有人時,需要排隊,
* 這樣線程就不能同時運行。
*
* 同步塊:有效的縮小同步範圍可以在保證並發安全的前提下提高代碼執行效率
* 同步塊可以更佳精確的控制需要同步的代碼片段
* */
public class Thread_SyncDemo02 {
public static void main(String[] args) {
final Shop shop = new Shop();
Thread t1 = new Thread(){
public void run(){
shop.buy();
}
};
Thread t2 = new Thread(){
public void run(){
shop.buy();
}
};
t1.start();
t2.start();
}
}
class Shop{
public void buy(){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在挑選衣服...");
try {
Thread.sleep(2000);
/*
* 同步塊需要指定監視器對象(上鎖的對象),只要保證該對象
* 多個線程看到的是同一個,那麽這些線程就不會同時執行同步塊中的代碼
*/
synchronized(this){
System.out.println(t.getName()+":正在試衣服...");
Thread.sleep(2000);
System.out.println(t.getName()+":試衣服結束!");
}
System.out.println(t.getName()+":結賬離開!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
運行結果:
Thread-1:正在挑選衣服...
Thread-0:正在挑選衣服...
Thread-1:正在試衣服...
Thread-1:試衣服結束!
Thread-0:正在試衣服...
Thread-1:結賬離開!
Thread-0:試衣服結束!
Thread-0:結賬離開!
*/
案例9:synchronized修飾靜態方法
/*
* 靜態方法上使用synchronized修飾,那麽該方法一定具有同步效果
* 靜態方法的同步監視器對象為當前靜態方法所屬類的類對象(Class的實例)
* JVM在加載每一個類的時候都會實例化一個且只會實例化一個Class的實例來
* 描述它,所以每個類有且只有一個Class的實例,靜態方法鎖的就是這個對象
*/
public class Thread_SyncDemo03 {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
Sync.dosome();
}
};
Thread t2 = new Thread(){
public void run(){
Sync.dosome();
}
};
t1.start();
t2.start();
}
}
class Sync{
public synchronized static void dosome(){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在運行dosome方法");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+":dosome方法運行完畢!");
}
}
/*
運行結果:
Thread-0:正在運行dosome方法
Thread-0:dosome方法運行完畢!
Thread-1:正在運行dosome方法
Thread-1:dosome方法運行完畢!
*/
案例10:互斥鎖
/*
* 互斥鎖
* 當使用synchronized修飾多段不同代碼時,同步監視器對象相同,
* 那麽這些代碼間就存在互斥,即:多個線程不能同時執行這些代碼
*/
public class Thread_SyncDemo04 {
public static void main(String[] args) {
final Aoo aoo = new Aoo();
Thread t1 = new Thread(){
public void run(){
aoo.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
aoo.methodB();
}
};
t1.start();
t2.start();
}
}
class Aoo{
public synchronized void methodA(){ //修飾方法
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在運行方法A...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+":方法A運行完畢!");
}
public void methodB(){
synchronized (this) { //修飾代碼塊
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在運行方法B...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+":方法B運行完畢!");
}
}
}
/*
運行結果:
Thread-0:正在運行方法A...
Thread-0:方法A運行完畢!
Thread-1:正在運行方法B...
Thread-1:方法B運行完畢!
*/
6)wait和notify
多線程之間需要協調工作。如:瀏覽器的一個顯示圖片的displayThread想要執行顯示圖片的任務,必須等待下載線程downloadThread將該圖片下載完畢,如果圖片還沒有下載完畢,displayThread可以暫停,當下載完畢再通知displayThread顯示圖片,這時displayThread線程繼續執行。
當條件不滿足,則線程等待。當條件滿足,則線程將被喚醒。這個機制得依賴wait和notify,等待機制與鎖機機制是密切關聯的。
5.線程池
1)線程安全API與非線程安全API
StringBuffer是同步的synchronized append();
StringBuilder不是同步的append();
StringBuffer在處理上稍遜於StringBuilder,但是其是線程安全的,當不存在並發時首選應當使用StringBuilder。
Vector和Hashtable是線程安全的,而ArrayList和HashMap則不是線程安全的。
對於集合而言,Collection是提供了幾個靜態方法,可以將集合或Map轉換為線程安全的。
- Collections.synchronizedList():獲取線程安全的List集合
- Collections.synchronizedMap():獲取線程安全的Map
案例11:線程安全API演示
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/*
* 將集合和Map轉換為線程安全的
* List常用實現類:ArrayList,LinkedList
* Set常用實現類:HashSet
* Map常用實現類:HashMap
* 以上都不是線程安全的
*
* 可以通過集合工具類:Collections將他們轉換為線程安全的
*/
public class ThreadApiDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("one");
list.add("two");
list.add("three");
System.out.println(list);
/*
* 將給定List集合轉換為線程安全的
*/
list = Collections.synchronizedList(list);
System.out.println("線程安全的List:"+list);
Set<String> set = new HashSet<String>(list); //將List內容轉換為Set,註意不能有重復元素,如果有重復元素則只保留一份
System.out.println(set);
/*
* 將Set集合轉換為線程安全的
*/
set = Collections.synchronizedSet(set);
System.out.println("線程安全的Set:"+set);
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("語文",90);
map.put("數學",100);
map.put("英語", 60);
System.out.println(map);
/*
* 將Map轉換為線程安全的
*/
map = Collections.synchronizedMap(map);
System.out.println("線程安全的Map:"+map);
}
}
/*
運行結果:
[one, two, three]
線程安全的List:[one, two, three]
[one, two, three]
線程安全的Set:[one, two, three]
{數學=100, 語文=90, 英語=60}
線程安全的Map:{數學=100, 語文=90, 英語=60}
*/
2)ExecutorService實現線程池
當一個程序中若創建大量線程,並在任務結束後銷毀,會給系統帶來過度資源消耗,以及過度切換線程的危險,從而可能導致系統崩潰,所以需要使用線程池來解決這個問題。ExecutorService是Java提供的用於管理線程池的類。
線程池的作用:1.控制線程數量;2.重用線程。
線程池的概念:首先創建一些線程,他們的集合成為線程池,當服務器接收到一個客戶請求後,就從線程池中取出空閑的線程為之服務,服務完畢後不關閉該線程,而是將該線程還回線程池中。
在線程池的編程模式下,任務是提交給整個線程池,而不是提交給某個線程,線程池在拿到任務後,它就在內部找空閑的線程再把任務交給內部空閑的線程,任務是提交給整個線程池,一個線程同時只能執行一個任務,但可以同時向一個線程池提交多個任務。
線程池有以下集中實現策略:
- Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池,但是以前構造的線程可用時將重用它們。
- Executors.newFixedThreadPool(int nThread):創建一個可重用固定線程集合的線程池,以共享的無界隊列方式來運行這些線程。
- Executors.newScheduledThreadPool(int corePoolSize):創建一個線程池,它可安排在給定延遲後運行或者定期的執行。
- Executors.newSingleThreadExecutor():創建一個使用單個worker線程的Executor,以無界隊列方式來運行該線程。
//創建一個固定線程數量的線程池:
...
ExecutorService threadPool
= Executors.newFixedThreadPool(30);//創建具有30個線程的線程池
Runnable r1 = new Runable(){
public void run(){
//線程體
}
};
threadPool.execute(r1);//將任務交給線程池,其會分配空閑線程來運行這個任務。
...
3)使用BlockingQueue
BlockingQueue是雙緩沖隊列。
在多線程並發時,若需要使用隊列,可以使用Queue,但是要解決一個問題就是同步,但同步操作會降低並發對Queue操作的效率。
BlockingQueue內部使用兩條隊列,可允許兩個線程同時向隊列一個做存儲,一個做取出操作。在保證並發安全的同時提高了隊列的存取效率。
雙緩沖隊列有以下幾種實現:
- ArrayBlockingDeque:規定大小的BlockingDeque,其構造函數必須帶一個int參數來指明其大小,其所含的對象是以FIFO(先進先出)順序排序的。
- LinkedBlockingDeque:大小不定的BlockingDeque,若其構造函數帶一個規定大小的參數,生成的BlockingDeque有大小限制,若不帶大小參數,所生成的BlockingDeque的大小由Integer.MAX_VALUE來決定,其所含的對象是以FIFO(先進先出)順序排序的。
- PriorityBlockingDeque:類似於LinkedBlockingDeque,但其所含對象的排序不是FIFO,而是依據對象的自然排序或者是噶歐早函數的Comparator決定的順序。
- SynchronousQueue:特殊的BlockingQueue,對其的操作必須是放和取交替完成的。
public static void main(String[] args) {
BlockingQueue<String> queue
= new LinkedBlockingDeque<String>();
try {
//queue.offer("A");//立即向隊列末尾追加元素
/*
* 向隊列末尾追加元素,指定可延遲5秒。
* 若5秒鐘內成功將元素加入隊列返回true
* 若超時後元素仍然沒有加入隊列則返回flase
*/
queue.offer("A",5,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(queue.poll());
}
案例12:線程池演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
* 線程池主要解決兩個問題:
* 1.控制線程數量
* 2.重用該線程
* 由於每個線程都需要資源消耗,且占用CPU計算時間,那麽線程數量過大會對系統資源消耗
* 產生映像,並且由於CPU過度切換導致系統拖慢,頻繁的創建線程和銷毀線程也會給系統帶
* 來不小的開銷,為此重用線程和控制線程數量是在使用線程時必須註意的問題。
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2); //創建2個線程的線程池
for(int i=0;i<5;i++){
Runnable runn = new Runnable(){
public void run(){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在運行任務...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+":任務運行完畢!");
}
};
/*
* 將任務交給線程池去運行,如果線程池中有空閑線程
* 那麽會立即分配線程來運行這個任務,若沒有,則會
* 加入線程池內部的隊列,等待線程來運行
*/
threadPool.execute(runn);
System.out.println("指派了一個任務...");
}
/*
* 如果不加shutdown方法,則任務運行結束後,線程池不會關閉會繼續等待下一次任務分配,
* 因為線程池是前臺進程,所以程序處於一直運行狀態。
*
* shutdown():當線程池中所有任務運行完畢後停止線程池。
* shutdownNow():強制將線程池中所有線程中斷,並停止線程池。
*/
// threadPool.shutdown();
threadPool.shutdownNow();
System.out.println("線程池停止了.");
}
}
/*
threadPool.shutdown():運行結果:
指派了一個任務...
指派了一個任務...
指派了一個任務...
指派了一個任務...
指派了一個任務...
pool-1-thread-1:正在運行任務...
pool-1-thread-2:正在運行任務...
線程池停止了.
pool-1-thread-1:任務運行完畢!
pool-1-thread-1:正在運行任務...
pool-1-thread-2:任務運行完畢!
pool-1-thread-2:正在運行任務...
pool-1-thread-1:任務運行完畢!
pool-1-thread-1:正在運行任務...
pool-1-thread-2:任務運行完畢!
pool-1-thread-1:任務運行完畢!
threadPool.shutdownNow()運行結果:
指派了一個任務...
指派了一個任務...
指派了一個任務...
指派了一個任務...
pool-1-thread-1:正在運行任務...
pool-1-thread-2:正在運行任務...
指派了一個任務...
線程池停止了.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at javase.day09.ThreadPoolDemo$1.run(ThreadPoolDemo.java:23)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
java.lang.InterruptedException: sleep interrupted
pool-1-thread-1:任務運行完畢!
at java.lang.Thread.sleep(Native Method)
at javase.day09.ThreadPoolDemo$1.run(ThreadPoolDemo.java:23)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
pool-1-thread-2:任務運行完畢!
*/
Java 多線程基礎