多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程並發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機、多核心處理器以及芯片級多處理(Chip-level multithreading)或同時多線程(Simultaneous multithreading)處理器。在一個程序中,這些獨立運行的程序片段叫作“線程”(Thread),利用它編程的概念就叫作“多線程處理(Multithreading)”。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。
一、多線程概要
1.1、多任務與單任務操作系統
多任務處理是指用戶可以在同一時間內運行多個應用程序,每個應用程序被稱作一個任務.Linux、windows就是支持多任務的操作系統,比起單任務系統它的功能增強了許多。
當多任務操作系統使用某種任務調度策略允許兩個或更多進程並發共享一個處理器時,事實上處理器在某一時刻只會給一件任務提供服務。因為任務調度機制保證不同任務之間的切換速度十分迅速,因此給人多個任務同時運行的錯覺。多任務系統中有3個功能單位:任務、進程和線程。
多任務處理有兩種類型:
- 基於進程
- 基於線程
進程是指一種“自包容”的運行程序,有自己的地址空間;線程是進程內部單一的一個順序控制流
基於進程的特點是允許計算機同時運行兩個或更多的程序。
基於線程的多任務處理環境中,線程是最小的處理單位。
基於線程所需的開銷更少
在多任務中,各個進程需要分配它們自己獨立的地址空間
多個線程可共享相同的地址空間並且共同分享同一個進程
進程間調用涉及的開銷比線程間通信多
線程間的切換成本比進程間切換成本低
1.2、進程與線程
進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位)
線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是cpu調度的最小單位)
線程和進程一樣分為五個階段:創建、就緒、運行、阻塞、終止。
多進程是指操作系統能同時運行多個任務(程序)。
多線程是指在同一程序中有多個順序流在執行。
每個正在系統上運行的程序都是一個進程。每個進程包含一到多個線程。進程也可能是整個程序或者是部分程序的動態執行。線程是一組指令的集合,或者是程序的特殊段,它可以在程序里獨立執行。也可以把它理解為代碼運行的上下文。所以線程基本上是輕量級的進程,它負責在單個程序里執行多任務。通常由操作系統負責多個線程的調度和執行。
線程和進程的區別在於,子進程和父進程有不同的代碼和數據空間,而多個線程則共享數據空間,每個線程有自己的執行堆棧和程序計數器為其執行上下文.多線程主要是為了節約CPU時間,發揮利用,根據具體情況而定. 線程的運行中需要使用計算機的內存資源和CPU。
在Java中,一個應用程序可以包含多個線程。每個線程執行特定的任務,並可與其他線程並發執行
多線程使系統的空轉時間最少,提高CPU利用率、
多線程編程環境用方便的模型隱藏CPU在任務間切換的事實
在Java程序啟動時,一個線程立刻運行,該線程通常稱為程序的主線程。
主線程的重要性體現在兩個方面:
它是產生其他子線程的線程。
通常它必須最后完成執行,因為它執行各種關閉動作。
class Mythread extends Thread { public static void main(String args[]) { Thread t= Thread.currentThread(); System.out.println("當前線程是: "+t); t.setName("MyJavaThread"); System.out.println("當前線程名是: "+t); try { for(int i=0;i<3;i++) { System.out.println(i); Thread.sleep(1500); } } catch(InterruptedException e) { System.out.println("主線程被中斷"); } } }
1.3、多線程的優點
- 使用線程可以把占據時間長的程序中的任務放到后台去處理
- 用戶界面可以更加吸引人,這樣比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度
- 程序的運行速度可能加快
- 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種情況下可以釋放一些珍貴的資源如內存占用等等。
- 多線程技術在IOS軟件開發中也有舉足輕重的位置。
- 線程應用的好處還有很多,就不一一說明了
一個采用了多線程技術的應用程序可以更好地利用系統資源。其主要優勢在於充分利用了CPU的空閑時間片,可以用盡可能少的時間來對用戶的要求做出響應,使得進程的整體運行效率得到較大提高,同時增強了應用程序的靈活性。更為重要的是,由於同一進程的所有線程是共享同一內存,所以不需要特殊的數據傳送機制,不需要建立共享存儲區或共享文件,從而使得不同任務之間的協調操作與運行、數據的交互、資源的分配等問題更加易於解決。
1.4、多線程的缺點
- 如果有大量的線程,會影響性能,因為操作系統需要在它們之間切換。
- 更多的線程需要更多的內存空間。
- 線程可能會給程序帶來更多“bug”,因此要小心使用。
- 線程的中止需要考慮其對程序運行的影響。
- 通常塊模型數據是在多個線程間共享的,需要防止線程死鎖情況的發生。
二、Timer和TimerTask
2.1. Timer和TimerTask
Timer是jdk中提供的一個定時器工具,使用的時候會在主線程之外起一個單獨的線程執行指定的計划任務,可以指定執行一次或者反復執行多次。
TimerTask是一個實現了Runnable接口的抽象類,代表一個可以被Timer執行的任務。
2.2. 一個Timer調度的例子
Daemon()程序是一直運行的服務端程序,又稱為守護進程。通常在系統后台運行,沒有控制終端,不與前台交互,Daemon程序一般作為系統服務使用。Daemon是長時間運行的進程,通常在系統啟動后就運行,在系統關閉時才結束。一般說Daemon程序在后台運行,是因為它沒有控制終端,無法和前台的用戶交互。Daemon程序一般都作為服務程序使用,等待客戶端程序與它通信。我們也把運行的Daemon程序稱作守護進程。
(1)Timer.schedule(TimerTask task,Date time)安排在制定的時間執行指定的任務。
(2)Timer.schedule(TimerTask task,Date firstTime ,long period)安排指定的任務在指定的時間開始進行重復的固定延遲執行.
(3)Timer.schedule(TimerTask task,long delay)安排在指定延遲后執行指定的任務.
(4)Timer.schedule(TimerTask task,long delay,long period)安排指定的任務從指定的延遲后開始進行重復的固定延遲執行.
(5)Timer.scheduleAtFixedRate(TimerTask task,Date firstTime,long period)安排指定的任務在指定的時間開始進行重復的固定速率執行.
(6)Timer.scheduleAtFixedRate(TimerTask task,long delay,long period)安排指定的任務在指定的延遲后開始進行重復的固定速率執行.
示例:
package com.zhangguo.thread; import java.util.Date; import java.util.Scanner; import java.util.Timer; import java.util.TimerTask; public class TimerDemo1 { public static void main(String[] args) { Timer timer1=new Timer("定時器1", false); //給定時器安排任務,延遲10毫秒執行,執行完后間隔3000毫秒執行 timer1.schedule(new TimerTask1("定時器A:"), 10, 3000); timer1.schedule(new TimerTask1("定時器B:"), 10, 1000); Scanner input=new Scanner(System.in); System.out.print("是否要結束任務:"); input.nextLine(); timer1.cancel(); //結束任務 System.out.println("定時任務結束了"); } } //定時任務 class TimerTask1 extends TimerTask{ private String taskName=""; public TimerTask1(String taskName) { this.taskName=taskName; } @Override public void run() { System.out.println(taskName+new Date()+""); } }
結果:
示例二:
package com.threaddemo; import java.util.Date; import java.util.Scanner; import java.util.Timer; import java.util.TimerTask; public class TimerSchedule { public static void main(String[] args) { Timer timer=new Timer(true); timer.schedule(new PrintTask(),0,3000); System.out.println("任意鍵結束"); Scanner input=new Scanner(System.in); input.nextLine(); timer.cancel(); System.out.println("結束了"); } } class PrintTask extends TimerTask{ @Override public void run() { System.out.println("時鍾任務"+new Date().toString()); } }
示例三:
1 import java.util.Timer; 2 import java.util.TimerTask; 3 4 public class TestTimer { 5 6 public static void main(String args[]){ 7 System.out.println("About to schedule task."); 8 new Reminder(3); 9 System.out.println("Task scheduled."); 10 } 11 12 public static class Reminder{ 13 Timer timer; 14 15 public Reminder(int sec){ 16 timer = new Timer(); 17 timer.schedule(new TimerTask(){ 18 public void run(){ 19 System.out.println("Time's up!"); 20 timer.cancel(); 21 } 22 }, sec*1000); 23 } 24 } 25 }
運行之后,在console會首先看到:
About to schedule task.
Task scheduled.
然后3秒鍾后,看到
Time's up!
從這個例子可以看出一個典型的利用timer執行計划任務的過程如下:
- new一個TimerTask的子類,重寫run方法來指定具體的任務,在這個例子里,我用匿名內部類的方式來實現了一個TimerTask的子類
- new一個Timer類,Timer的構造函數里會起一個單獨的線程來執行計划任務。jdk的實現代碼如下:
1 public Timer() { 2 this("Timer-" + serialNumber()); 3 } 4 5 public Timer(String name) { 6 thread.setName(name); 7 thread.start(); 8 }
- 調用相關調度方法執行計划。這個例子調用的是schedule方法。
- 任務完成,結束線程。這個例子是調用cancel方法結束線程。
2.3. 如何終止Timer線程
默認情況下,創建的timer線程會一直執行,主要有下面四種方式來終止timer線程:
- 調用timer的cancle方法
- 把timer線程設置成daemon線程,(new Timer(true)創建daemon線程),在jvm里,如果所有用戶線程結束,那么守護線程也會被終止,不過這種方法一般不用。
- 當所有任務執行結束后,刪除對應timer對象的引用,線程也會被終止。
- 調用System.exit方法終止程序
2.4. 關於cancle方式終止線程
這種方式終止timer線程,jdk的實現比較巧妙,稍微說一下。
首先看cancle方法的源碼:
1 public void cancel() { 2 synchronized(queue) { 3 thread.newTasksMayBeScheduled = false; 4 queue.clear(); 5 queue.notify(); // In case queue was already empty. 6 } 7 }
沒有顯式的線程stop方法,而是調用了queue的clear方法和queue的notify方法,clear是個自定義方法,notify是Objec自帶的方法,很明顯是去喚醒wait方法的。
再看clear方法:
1 void clear() { 2 // Null out task references to prevent memory leak 3 for (int i=1; i<=size; i++) 4 queue[i] = null; 5 6 size = 0; 7 }
clear方法很簡單,就是去清空queue,queue是一個TimerTask的數組,然后把queue的size重置成0,變成empty.還是沒有看到顯式的停止線程方法,回到最開始new Timer的時候,看看new Timer代碼:
1 public Timer() { 2 this("Timer-" + serialNumber()); 3 } 4 5 public Timer(String name) { 6 thread.setName(name); 7 thread.start(); 8 }
看看這個內部變量thread:
1 /** 2 * The timer thread. 3 */ 4 private TimerThread thread = new TimerThread(queue);
不是原生的Thread,是自定義的類TimerThread.這個類實現了Thread類,重寫了run方法,如下:
1 public void run() { 2 try { 3 mainLoop(); 4 } finally { 5 // Someone killed this Thread, behave as if Timer cancelled 6 synchronized(queue) { 7 newTasksMayBeScheduled = false; 8 queue.clear(); // Eliminate obsolete references 9 } 10 } 11 }
最后是這個mainLoop方法,這方法比較長,截取開頭一段:
1 private void mainLoop() { 2 while (true) { 3 try { 4 TimerTask task; 5 boolean taskFired; 6 synchronized(queue) { 7 // Wait for queue to become non-empty 8 while (queue.isEmpty() && newTasksMayBeScheduled) 9 queue.wait(); 10 if (queue.isEmpty()) 11 break; // Queue is empty and will forever remain; die
可以看到wait方法,之前的notify就是通知到這個wait,然后clear方法在notify之前做了清空數組的操作,所以會break,線程執行結束,退出。
2.5. 反復執行一個任務
通過調用三個參數的schedule方法實現,最后一個參數是執行間隔,單位毫秒。
2.6. schedule VS. scheduleAtFixedRate
這兩個方法都是任務調度方法,他們之間區別是,schedule會保證任務的間隔是按照定義的period參數嚴格執行的,如果某一次調度時間比較長,那么后面的時間會順延,保證調度間隔都是period,而scheduleAtFixedRate是嚴格按照調度時間來的,如果某次調度時間太長了,那么會通過縮短間隔的方式保證下一次調度在預定時間執行。舉個栗子:你每個3秒調度一次,那么正常就是0,3,6,9s這樣的時間,如果第二次調度花了2s的時間,如果是schedule,就會變成0,3+2,8,11這樣的時間,保證間隔,而scheduleAtFixedRate就會變成0,3+2,6,9,壓縮間隔,保證調度時間。
2.7. 一些注意點
- 每一個Timer僅對應唯一一個線程。
- Timer不保證任務執行的十分精確。
- Timer類的線程安全的。
三、多線程的實現方式
通過以下兩種方法創建 Thread 對象:
3.1、繼承Thread
Java中“一切皆對象”,線程也被封裝成一個對象。我們可以通過繼承Thread類來創建線程。線程類中的的run()方法包含了該線程應該執行的指令。我們在衍生類中覆蓋該方法,以便向線程說明要做的任務:
聲明一個 Thread 類的子類,並覆蓋 run() 方法。
class mythread extends Thread { public void run( ) {/* 覆蓋該方法*/ } }
這里繼承Thread類的方法是比較常用的一種,如果說你只是想起一條線程。沒有什么其它特殊的要求,那么可以使用Thread.(筆者推薦使用Runable,后頭會說明為什么)。下面來看一個簡單的實例
package com.zhangguo.thread; public class ThreadDemo1 { public static void main(String[] args) { System.out.println("線程示例開始"); //創建線程對象 MyThread threadA=new MyThread("線程A"); MyThread threadB=new MyThread("線程B"); //開始運行 threadA.start(); threadB.start(); System.out.println("線程示例結束"); } } /**定義線程類*/ class MyThread extends Thread { private String name; public MyThread(String name) { this.name=name; } @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(this.name+":"+i); } } }
結果1:
線程示例開始 線程示例結束 線程A:1 線程A:2 線程B:1 線程B:2 線程A:3 線程A:4 線程A:5 線程A:6 線程B:3 線程B:4 線程A:7 線程A:8 線程A:9 線程A:10 線程B:5 線程B:6 線程B:7 線程B:8 線程B:9 線程B:10
結果2:
線程示例開始 線程示例結束 線程A:1 線程A:2 線程A:3 線程A:4 線程A:5 線程A:6 線程B:1 線程B:2 線程B:3 線程B:4 線程B:5 線程B:6 線程B:7 線程B:8 線程B:9 線程B:10 線程A:7 線程A:8 線程A:9 線程A:10
說明:
程序啟動運行main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建。隨着調用MyThread的兩個對象的start方法,另外兩個線程也啟動了,這樣,整個應用就在多線程下運行。
注意:start()方法的調用后並不是立即執行多線程代碼,而是使得該線程變為可運行態(Runnable),什么時候運行是由操作系統決定的。
從程序運行的結果可以發現,多線程程序是亂序執行。因此,只有亂序執行的代碼才有必要設計為多線程。
Thread.sleep()方法調用目的是不讓當前線程休眠一段時間,暫停運行。
實際上所有的多線程代碼執行順序都是不確定的,每次執行的結果都是隨機的。
3.2、實現Runnable 接口
實現多線程的另一個方式是實施Runnable接口,並提供run()方法。實施接口的好處是容易實現多重繼承(multiple inheritance)。然而,由於內部類語法,繼承Thread創建線程可以實現類似的功能。我們在下面給出一個簡單的例子,而不深入:
聲明一個實現 Runnable 接口的類,並實現 run() 方法。
class mythread implements Runnable{ public void run( ) {/* 實現該方法*/ }
}
采用Runnable也是非常常見的一種,我們只需要重寫run方法即可。下面也來看個實例。
示例:
package com.zhangguo.thread; public class RunableDemo { public static void main(String[] args) { System.out.println("線程示例開始"); //創建線程對象 Thread threadA=new Thread(new MyRunnable("線程A")); Thread threadB=new Thread(new MyRunnable("線程B")); threadA.start(); threadB.start(); System.out.println("線程示例結束"); } } /**定義線程類*/ class MyRunnable implements Runnable { private String name; public MyRunnable(String name) { this.name=name; } @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(this.name+":"+i); } } }
結果1:
線程示例開始 線程A:1 線程A:2 線程A:3 線程A:4 線程示例結束 線程A:5 線程A:6 線程A:7 線程A:8 線程A:9 線程A:10 線程B:1 線程B:2 線程B:3 線程B:4 線程B:5 線程B:6 線程B:7 線程B:8 線程B:9 線程B:10
結果2:
線程示例開始 線程A:1 線程A:2 線程A:3 線程A:4 線程A:5 線程A:6 線程A:7 線程A:8 線程A:9 線程A:10 線程示例結束 線程B:1 線程B:2 線程B:3 線程B:4 線程B:5 線程B:6 線程B:7 線程B:8 線程B:9 線程B:10
說明:
Thread2類通過實現Runnable接口,使得該類有了多線程類的特征。run()方法是多線程程序的一個約定。所有的多線程代碼都在run方法里面。Thread類實際上也是實現了Runnable接口的類。
在啟動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然后調用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是擴展Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
思考題:
package com.zhangguo.thread; public class RunnableDemo2 { public int n=0; public static void main(String[] args) { RunnableDemo2 obj=new RunnableDemo2(); new Thread(new Counter(obj)).start(); new Thread(new Counter(obj)).start(); new Thread(new Counter(obj)).start(); System.out.println(obj.n); } } class Counter implements Runnable{ RunnableDemo2 obj; public Counter(RunnableDemo2 obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { obj.n++; } } }
結果1:
5796
結果2:
4615
結果3:
20000
結果為什么是不確認的,怎樣保證正確的結果。
3.3、Thread和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
總結:
實現Runnable接口比繼承Thread類所具有的優勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立
4):線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類
提醒一下大家:main方法其實也是一個線程。在java中所以的線程都是同時啟動的,至於什么時候,哪個先執行,完全看誰先得到CPU的資源。
在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個jVM實習在就是在操作系統中啟動了一個進程。
四、線程狀態與調度
4.1、線程的狀態
1、新建狀態(New):新創建了一個線程對象。
2、就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4、阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
4.2、線程調度
線程的調度
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中已經廢除,不再介紹。因為有死鎖傾向。
4.3、線程類的一些常用方法
sleep(): 強迫一個線程睡眠N毫秒。
isAlive(): 判斷一個線程是否存活。
join(): 等待線程終止。
activeCount(): 程序中活躍的線程數。
enumerate(): 枚舉程序中的線程。
currentThread(): 得到當前線程。
isDaemon(): 一個線程是否為守護線程。
setDaemon(): 設置一個線程為守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
setName(): 為線程設置一個名稱。
wait(): 強迫一個線程等待。
notify(): 通知一個線程繼續運行。
setPriority(): 設置一個線程的優先級
五、線程同步
在多線程應用中,考慮不同線程之間的數據同步和防止死鎖。當兩個或多個線程之間同時等待對方釋放資源的時候就會形成線程之間的死鎖。為了防止死鎖的發生,需要通過同步來實現線程安全。在Java中可用synchronized關鍵字。
5.1、線程同步的問題
package com.zhangguo.thread; public class RunnableDemo2 { public static void main(String[] args) throws Exception { NumberBox obj=new NumberBox(); Thread t1=new Thread(new Counter(obj)); Thread t2=new Thread(new Counter(obj)); Thread t3=new Thread(new Counter(obj)); t1.start(); t2.start(); t3.start(); Thread.sleep(5000); System.out.println(obj.n); } } class NumberBox{ public int n=0; } class Counter implements Runnable{ NumberBox obj; public Counter(NumberBox obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { obj.n=obj.n+1; } System.out.println("n="+obj.n); } }
結果:
n=20226 n=30328 n=50328 50328
因為obj.n=obj.n+1是並行執行的,資源存在爭用的問題,需要同步,否則結果不正確。
5.2、synchronized
5.2.1、同步方法
synchronized關鍵字修飾的方法。
由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,
內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。
public synchronized void save(){ }
注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類
示例:
package com.zhangguo.thread.sync; public class SyncDemo2 { public static void main(String[] args) throws Exception { Box obj = new Box(); /*多線程單實例*/ Inc incObj=new Inc(obj); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); } } class Box { public int n = 0; } class Inc implements Runnable { Box obj; public Inc(Box obj) { this.obj = obj; } @Override public void run() { counter(); } /** 同步方法,鎖的是調用該方法的對象 */ public synchronized void counter() { for (int i = 0; i < 20000; i++) { obj.n = obj.n + 1; } System.out.println("n=" + obj.n); } }
結果:
n=20000 n=40000 n=60000
5.2.2、同步代碼塊
synchronized關鍵字修飾的語句塊。
被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步
代碼如:
synchronized(object){ }
注:同步是一種高開銷的操作,因此應該盡量減少同步的內容。
通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。
示例:
package com.zhangguo.thread; public class SyncDemo1 { public static void main(String[] args) throws Exception { Box obj=new Box(); Thread t1=new Thread(new Inc(obj)); Thread t2=new Thread(new Inc(obj)); Thread t3=new Thread(new Inc(obj)); t1.start(); t2.start(); t3.start(); } } class Box{ public int n=0; } class Inc implements Runnable{ Box obj; public Inc(Box obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { synchronized (obj) { obj.n=obj.n+1; } } System.out.println("n="+obj.n); } }
結果:
n=43533 n=52590 n=60000
5.3、使用特殊域變量(volatile)實現線程同步
['vɑːlətl] 不穩定的;反復無常的;易揮發的
a.volatile關鍵字為域變量的訪問提供了一種免鎖機制,
b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新,
c.因此每次使用該域就要重新計算,而不是使用寄存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量
//只給出要修改的代碼,其余代碼與上同 class Bank { //需要同步的變量加上volatile private volatile int account = 100; public int getAccount() { return account; } //這里不再需要synchronized public void save(int money) { account += money; } }
注:多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。
用final域,有鎖保護的域和volatile域可以避免非同步的問題。
在使用volatile關鍵字時要慎重,並不是只要簡單類型變量使用volatile修飾,對這個變量的所有操作都是原來操作,當變量的值由自身的上一個決定時,如n=n+1、n++等,volatile關鍵字將失效,只有當變量的值和自身上一個值無關時對該變量的操作才是原子級別的,如n = m + 1,這個就是原級別的。所以在使用volatile關鍵時一定要謹慎,如果自己沒有把握,可以使用synchronized來代替volatile。
示例:
package com.zhangguo.thread.syncv; public class SyncDemo3 { public static void main(String[] args) throws Exception { /* 多線程單實例 */ Inc incObj = new Inc(); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(incObj.n); } } class Inc implements Runnable { public volatile int n = 0; @Override public void run() { counter(); } public void counter() { for (int i = 0; i < 20000; i++) { this.n = this.n + 1; } System.out.println("n=" + this.n); } }
結果:
n=34329 n=38177 n=33705 38177
所以,volatile並不能保證數據是同步的,只能保證線程得到的數據是最新的。
那么,我們應該在什么情況下使用volatile關鍵字呢?
其實很簡單.只要符合以下兩個條件就能使用volatile,並且能收到很不錯的效果。
1.對變量的寫入不依賴變量的當前值,或者只有一個線程更新變量的值
2.該變量不會和其他狀態變量一起被列為不變性條件中
5.4.使用重入鎖實現線程同步
在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。
ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,
它與使用synchronized方法和快具有相同的基本行為和語義,並且擴展了其能力
ReenreantLock類的常用方法有:
ReentrantLock() : 創建一個ReentrantLock實例
lock() : 獲得鎖
unlock() : 釋放鎖
注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用
代碼實例:
package com.zhangguo.thread.syncv; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SyncDemo3 { public static void main(String[] args) throws Exception { /* 多線程單實例 */ Inc incObj = new Inc(); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(incObj.n); } } class Inc implements Runnable { public int n = 0; private Lock lock = new ReentrantLock(); @Override public void run() { counter(); } public void counter() { for (int i = 0; i < 20000; i++) { // 鎖 lock.lock(); try { this.n = this.n + 1; } finally { lock.unlock(); } } System.out.println("n=" + this.n); } }
結果:
n=26058 n=45353 n=60000 60000
注:關於Lock對象和synchronized關鍵字的選擇:
a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,
能夠幫助用戶處理所有與鎖相關的代碼。
b.如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼
c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖
5.5.使用局部變量實現線程同步
如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,
副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
ThreadLocal 類的常用方法
ThreadLocal() : 創建一個線程本地變量
get() : 返回此線程局部變量的當前線程副本中的值
initialValue() : 返回此線程局部變量的當前線程的"初始值"
set(T value) : 將此線程局部變量的當前線程副本中的值設置為value
例如:
在上面例子基礎上,修改后的代碼為:
代碼實例:
//只改Bank類,其余代碼與上同 public class Bank{ //使用ThreadLocal類管理共享變量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public void save(int money){ account.set(account.get()+money); } public int getAccount(){ return account.get(); } }
注:ThreadLocal與同步機制
a.ThreadLocal與同步機制都是為了解決多線程中相同變量的訪問沖突問題。
b.前者采用以"空間換時間"的方法,后者采用以"時間換空間"的方式
5.6.使用阻塞隊列實現線程同步
前面5種同步方式都是在底層實現的線程同步,但是我們在實際開發當中,應當盡量遠離底層結構。
使用javaSE5.0版本中新增的java.util.concurrent包將有助於簡化開發。
本小節主要是使用
LinkedBlockingQueue<E>
來實現線程的同步
LinkedBlockingQueue<E>是一個基於已連接節點的,范圍任意的blocking queue。
隊列是先進先出的順序(FIFO),關於隊列以后會詳細講解~
LinkedBlockingQueue 類常用方法
LinkedBlockingQueue() : 創建一個容量為Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在隊尾添加一個元素,如果隊列滿則阻塞
size() : 返回隊列中的元素個數
take() : 移除並返回隊頭元素,如果隊列空則阻塞
代碼實例:
實現商家生產商品和買賣商品的同步
1 package com.xhj.thread; 2 3 import java.util.Random; 4 import java.util.concurrent.LinkedBlockingQueue; 5 6 /** 7 * 用阻塞隊列實現線程同步 LinkedBlockingQueue的使用 8 * 9 * @author XIEHEJUN 10 * 11 */ 12 public class BlockingSynchronizedThread { 13 /** 14 * 定義一個阻塞隊列用來存儲生產出來的商品 15 */ 16 private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); 17 /** 18 * 定義生產商品個數 19 */ 20 private static final int size = 10; 21 /** 22 * 定義啟動線程的標志,為0時,啟動生產商品的線程;為1時,啟動消費商品的線程 23 */ 24 private int flag = 0; 25 26 private class LinkBlockThread implements Runnable { 27 @Override 28 public void run() { 29 int new_flag = flag++; 30 System.out.println("啟動線程 " + new_flag); 31 if (new_flag == 0) { 32 for (int i = 0; i < size; i++) { 33 int b = new Random().nextInt(255); 34 System.out.println("生產商品:" + b + "號"); 35 try { 36 queue.put(b); 37 } catch (InterruptedException e) { 38 // TODO Auto-generated catch block 39 e.printStackTrace(); 40 } 41 System.out.println("倉庫中還有商品:" + queue.size() + "個"); 42 try { 43 Thread.sleep(100); 44 } catch (InterruptedException e) { 45 // TODO Auto-generated catch block 46 e.printStackTrace(); 47 } 48 } 49 } else { 50 for (int i = 0; i < size / 2; i++) { 51 try { 52 int n = queue.take(); 53 System.out.println("消費者買去了" + n + "號商品"); 54 } catch (InterruptedException e) { 55 // TODO Auto-generated catch block 56 e.printStackTrace(); 57 } 58 System.out.println("倉庫中還有商品:" + queue.size() + "個"); 59 try { 60 Thread.sleep(100); 61 } catch (Exception e) { 62 // TODO: handle exception 63 } 64 } 65 } 66 } 67 } 68 69 public static void main(String[] args) { 70 BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); 71 LinkBlockThread lbt = bst.new LinkBlockThread(); 72 Thread thread1 = new Thread(lbt); 73 Thread thread2 = new Thread(lbt); 74 thread1.start(); 75 thread2.start(); 76 77 } 78 79 }
注:BlockingQueue<E>定義了阻塞隊列的常用方法,尤其是三種添加元素的方法,我們要多加注意,當隊列滿時:
add()方法會拋出異常
offer()方法返回false
put()方法會阻塞
5.7.使用原子變量實現線程同步
需要使用線程同步的根本原因在於對普通變量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作
即-這幾種行為要么同時完成,要么都不完成。
在java的util.concurrent.atomic包中提供了創建了原子類型變量的工具類,使用該類可以簡化線程同步。其中AtomicInteger
表可以用原子方式更新int的值,可用在應用程序中(如以原子方式增加的計數器),
但不能用於替換Integer;可擴展Number,允許那些處理機遇數字類的工具和實用工具進行統一訪問。
AtomicInteger類常用方法:
AtomicInteger(int initialValue) : 創建具有給定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式將給定值與當前值相加
get() : 獲取當前值
代碼實例:
只改Bank類,其余代碼與上面第一個例子同
1 class Bank { 2 private AtomicInteger account = new AtomicInteger(100); 3 4 public AtomicInteger getAccount() { 5 return account; 6 } 7 8 public void save(int money) { 9 account.addAndGet(money); 10 } 11 }
補充--原子操作主要有:
對於引用變量和大多數原始變量(long和double除外)的讀寫操作;
對於所有使用volatile修飾的變量(包括long和double)的讀寫操作。
六、作業
6.1、每隔5秒向c:\logs目錄下寫入一個文件(yyyyMMddHHmmss.txt),內容為一個GUID,總文件數不超過100個,保留最新的文件。
6.2、在項目中增加一個數據庫自動備份功能,每5個小時備份一次,可以刪除備份文件。
在監聽器的contextInitialized事件中初始化任務
6.3、實現1+2+3+.....1000000,使用多線程分段累加后合並結果,並測試使用單線程與多線程所耗時的差別。