從計算機操作系統的發展來看,經歷了這樣的兩個階段
單線程處理:最傳統的DOS系統中只要有病毒出現,則立即有反映,因爲在DOS系統中屬於單進程處理,即:在同一個時間段上只有一個程序在執行。
多線程處理:windows操作系統是一個多進程,例如,假設在windows中出現病毒了,則系統一樣可以使用
進程:正在進行中的程序。
那麼對於資源來講,所有的IO設備、CPU等等只有一個,那麼對於多線程處理來講,在同一個時間段上會有多個程序運行,但是在同一個時間點上只能有一個程序運行。
線程是在進程基礎上的進一步劃分,例子:word中的拼寫檢查,是在word整個程序
運行中運行 (word是一個進程拼寫檢查可以當作一個線程,一個進程可以有多個線程,就類似於,開了一個遊戲,遊戲看做一個進程,遊戲裏面的玩家看作線程,遊戲沒有肯定玩家就沒了,但是遊戲裏面玩家沒了,遊戲卻還在。所以說線程是在進程基礎上進一部劃分)
所以,如果進程消失了,則線程消失了,而如果線程消失的話,則進程依然會執行,未必會消失,java本身是屬於多線程的操作語言,所以提供了線程的處理機制。
一個進程默認一個線程(main);
進程和線程切換,誰耗內存。
進程消耗性能一些,線程是在進程中切換不耗內存。
線程是CPU的最小執行單元。
所謂的線程是一組指令的集合。
線程實現的兩種方式在java中可以有兩種方式實現多線程操作,一種是是繼承Thread類,另外一種是實現Runnable 接口。
Thread是在java.lang包中定義的。
一個類只要繼承了Thread類,同時覆寫了本類中的run()方法 ,則就可以實現多線程的操作了。
package com.cym.thread;
public class MyThread extends Thread {
private String name;
public MyThread(String name){
this.name = name;
}
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(name + ":" + i);
}
}
}
以上的類已經完成了多線程的操作類,那麼下面就啓動線程
package com.cym.thread;
public class Main {
public static void main(String[]args) {
// TODO Auto-generated method stub
MyThreadmt1 = new MyThread("A");
MyThreadmt2 = new MyThread("B");
mt1.run(); // 執行線程體
mt2.run(); // 執行線程體
}
}
但是、此時執行可以發現非常的有規律,先執行玩第一個對象的,再執行第二個對象,也就是說並沒有現實交互的運行。
從JDK文檔可以發現,一旦調用start()方法,則會通過JVM找到run()方法
publicvoid start()
使用start啓動線程
package com.cym.thread;
public class Main {
public static void main(String[]args) {
// TODO Auto-generated method stub
MyThread mt1 = new MyThread("A");
MyThread mt2 = new MyThread("B");
mt1.start(); // 執行線程體
mt2.start(); // 執行線程體
}
}
此時程序已經可以進程交互式的運行了。
但是需要思考的是爲什麼要使用start()方法使用啓動多線程?
在JDK的安裝路徑下,src.zip是全部的java源程序,通過此代碼找到Thread 類中的start()方法定義 :
public synchronized void start() { // 定義的start()方法
if (started) // 判斷線程是否已經啓動
throw new IllegalThreadStateException();
Stared = true; // 如果沒有啓動則修改狀態
start0(); // 使用native 關鍵字聲明的的方法,沒有方法體.
if (stopBeforeStart) {
stop0(throwableFromStop);
}
}
private native void start0(); // 使用native 關鍵字聲明的方法,沒有方法體。
操作系統有很多,Windows、Linux、UNLX,既然多線程操作中要進行CPU資源的搶佔,也就是說要等待CPU的調度,那麼這些調度的操作是由各種操作系統的低層實現的,
所以在java程序中根本就沒法實現,那麼此時java的設計者定義了native關鍵字,使用此
關鍵字表示可以調用操作系統的底層函數,那麼這種技術又稱作爲JNI技術(java nativeinterface)
而且,此方法在執行的時候將調用run()方法完成,有系統默認調用的。
但是第一中操作中有一個最大的限制,一個類只能繼承一個父類。
Runnable接口
在實際的開發中一個多線程的操作類很少去使用thread類完成,而是通過Runnable接口完成。
觀察runnable接口定義
publicinterface Runnable{
Public void run();
}
如果說一個類要實現此接口那麼,就要實現run()方法;
package com.cym.runnable;
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name){
this.name = name;
}
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(name + ":" + i);
}
}
}
完成之後,下面啓動多線程
但是在現在使用Runnable的定義中並沒有start()方法,而只有Thread 類中才有.
在Thread類中存以下的一個構造方法:
publicThread(Runnable target)
此構造方法接收Runnable的子類實例,也就是說現在可以通過Thread 類來啓動Runnable現實的多線程,
package com.cym.runnable;
public class Main {
public static void main(String[]args) {
// TODO Auto-generated method stub
MyRunnable mr1 = new MyRunnable("A");
MyRunnable mr2 = new MyRunnable("B");
new Thread(mr1).start(); // 執行線程體
new Thread(mr2).start(); // 執行線程體
}
}
以上的操作代碼也屬於交替的運行,所以此時程序也實現了多線程的操作。
兩種方式的區別及練習
在程序的開發中要是多線程則肯定永遠以實現Runnable接口爲正統操作,因爲實現Runnable接口相比繼承Thread類有如下好處:
1避免了繼承的侷限,一個類可以同時現實多個接口。
2適合於資源共享
以買票的程序爲例,通用Thread類完成
package com.cym.threadticket;
public class MyThread extends Thread {
private int ticket = 5;
public void run(){
for (int i = 0; i < 100; i++) {
if(this.ticket > 0){
System.out.println("買票" + this.ticket--);
}
}
}
}
下面建立三個線程同時賣票
package com.cym.threadticket;
public class Main {
public static void main(String[]args) {
// TODO Auto-generated method stub
MyThread mt1 = new MyThread(); // 一個線程
MyThread mt2 = new MyThread(); // 一個線程
MyThread mt3 = new MyThread(); // 一個線程
mt1.start(); // 開始買票
mt2.start(); // 開始買票
mt3.start(); // 開始買票
}
}
發現一共買了15張票,但是實際上只有5張票,所以證明每一個線程都賣自己的票沒有達到資源共享的目的。
如果我們通過實現Runnable接口的話,那麼我們就可以現實資源共享的目的
package com.cym.runnableticket;
public class MyRunnable implements Runnable {
private int ticket = 5;
public void run(){
for (int i = 0; i < 100; i++) {
if(this.ticket > 0){
System.out.println("買票" + this.ticket--);
}
}
}
}
編寫多個線程進行賣票
package com.cym.runnableticket;
public class Main {
public static void main(String[]args) {
// TODO Auto-generated method stub
MyRunnable mr1 = new MyRunnable();
new Thread(mr1).start(); // 買票
new Thread(mr1).start(); // 買票
new Thread(mr1).start(); // 買票
}
}
雖然現在程序中有三個線程,但是一共才賣出去5張票。也就是說使用Runnable現實的多線程可以達到資源共享的目的。
實際上Runnable接口和Thread類還是有聯繫的
publicclass Thread
extendsObject
implementsRunnable
發現thread類也是實現了Runnable
一個接口同時有兩個子類(假設是A,B),把A類放在B類之中,那麼B類完成更多的功能但是最終結構還是以A類爲準
這是一個典型的代理設計。
線程的操作方法
對於線程來講所有的操作方法都在Thread類中定義的,所以如果想明確的理解操作方法,則肯定要從Thread類着手。
設置和取得名字在Thread類中存在以下的幾個方法可以設置和取得名字,
設置名字:
set:public finalvoid setName(String name);
構造:
public Thread(Runnable target, String name);
public Thread(String name);
取得名字:
public final StringgetName();
在線程的操作中以爲其操作的不確定性,所以提供了一個方法,可以取得當前的操作線程:
public static Thread currentThread()
對於線程的名字一般在啓動前進行設置,最好不要設置相同的名字,最好不要爲一個線程改名字。
Public class MyThread implements Runnable {
Public voidrun(){
For(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName + “線程正在運行。”);
}
}
}
那麼測試一下,上面是取得了線程的名字。
Main:
MyThread mt =new MyThread() // runnable子類實例
New Thread(mt,“線程A”).start();
New Thread(mt,“線程B”).start();
New Thread(mt,“線程C”).start();
明白代碼的作用之後,再看如下的程序:
Main:
MyThread mt =new MyThread() // runnable子類實例
New Thread(mt,“自定義線程”).start();
Mt.run(); // 直接通過對象調用
得出一個結論,在程序運行時主方法實際上就是一個主線程。
一直強調java是個多線程的操作語言,那麼它是如何實現的呢?
實際上對於java來講,每一次執行java命令對於操作系統來講都是啓動一個JVM的進程,
那麼主方法實際上只是這個進程上的進一步劃分。
問題:
在java執行一個java程序至少啓動幾個線程?
至少啓動兩個:main、gc
線程的休眠讓一個線程稍微小小的休息一下,之後起來繼續工作,稱爲休眠:
public staticvoid sleep(long millis,int nanos) throws InterruptedException
在使用方法時需要try..catch處理
package com.cym.sleep;
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"線程正在運行" );
}
}
}
Main:
MyThread mt = new MyThread();
new Thread(mt,"線程A").start();
new Thread(mt,"線程B").start();
程序的執行將變緩慢起來。
線程的中斷在sleep()方法中存在InterruptException,那麼會造成此異常的方法就是中斷:
public void interrupt()
package com.cym.interrupt;
public class Main {
public static void main(String[]args) {
MyThread mt = new MyThread();
Thread t = new Thread(mt,"自定義線程");
t.start();
t.interrupt(); // 中斷線程
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("1進入run()");
try {
System.out.println("2準備睡眠20秒");
Thread.sleep(2000);
System.out.println("3睡眠20秒完成");
} catch (InterruptedException e) {
System.out.println("4中斷線程");
return;// 返回方法調用處
}
System.out.println(Thread.currentThread().getName()+ "線程正在執行");
System.out.println("5退出run()");
}
}
線程的優先級在多線程的操作中,所有的代碼實際上都是存在優先級的,優先級高的就有可能先執行。在線程中使用以後的方法設置:public final void setPriority(int newPriority)
最高:public static final intMAX_PRIORITY
中等:public static final intNORM_PRIORITY
最低:public static final intMIN_PRIORITY
package com.cym.priority;
public class Main {
public static void main(String[]args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"A線程");
Thread t2 = new Thread(mt,"B線程");
Thread t3 = new Thread(mt,"C線程");
t2.setPriority(Thread.MIN_PRIORITY);//設置成最低優先級
t3.setPriority(Thread.MAX_PRIORITY);//設置最高優先級
t1.setPriority(Thread.NORM_PRIORITY);//設置成中等優先級
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"線程在運行");
}
}
}
各個線程可以設置優先級,那麼主方法的優先級呢?
package com.cym.priority;
public class TestPriorityMain {
public static void main(Stringargs[]){
System.out.println(Thread.currentThread().getPriority());//5
System.out.println(Thread.MAX_PRIORITY); //10
System.out.println(Thread.NORM_PRIORITY);//5
System.out.println(Thread.MIN_PRIORITY);//1
}
}
從輸出結果可以發現,主方法的優先級是普通的優先級
同步和死鎖(理解)問題的引出package com.cym.syn;
class MyTickerTreadimplements Runnable{ // Runnable接口
private int ticker = 5; // 一共才5張票
public void run() { // 覆寫run()方法
for (int i = 0; i < 100; i++) { // 表示循環10次
if(ticker > 0){
try {
Thread.sleep(300); //延遲
} catch (InterruptedException e) {
// TODO Auto-generated catchblock
e.printStackTrace();
} // 延遲
System.out.println("賣票:ticker = " + this.ticker--);
}
}
}
}
public class Main {
public static void main(String[]args) {
MyTickerTread mt = new MyTickerTread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
最終結果:
賣票:ticker =5
賣票:ticker =4
賣票:ticker =3
賣票:ticker =2
賣票:ticker =1
賣票:ticker =0
賣票:ticker =-1
從運行結果上可以發現,程序中加入了延遲操作,所以在運行的最後出現了負數的情況,那麼爲什麼現在會產生這樣的問題?
從上面的操作代碼可以發現對於票數的操作步驟如下:
1判斷票數是否大於0,大於0則表示還有票可以賣
2如果票數大於0,則賣票出去
但是,在上面的操作代碼中,在第一步和第二步之間加入了延遲操作,那麼一個線程就有可能在還沒有對票數進行操作的之前,其他線程就已經講票數減少了,這樣一來就會出現票數爲負的情況。
class MyTickerTreadimplements Runnable{ // Runnable接口
private int ticker = 5; // 一共才5張票
public void run() { // 覆寫run()方法
for (int i = 0; i < 100; i++) { // 表示循環10次
if(ticker > 0){
try {
Thread.sleep(300); //延遲 由於延遲的線程,使得第一個線程進來的時候,還沒開始--操作,第2個線程又進來了,也就是從5還沒--,第二個線程進來的時候又是5--
} catch (InterruptedException e) {
// TODO Auto-generated catchblock
e.printStackTrace();
} // 延遲
System.out.println("賣票:ticker = " + this.ticker--);
}
}
}
}
如果想解決這樣的問題,就必須同步,所謂的同步就是指多個操作在同一時間段內只能有一個線程進行,其他的線程要等待此線程要等待此線程完成之後纔可以繼續執行。
在java中可以通過代碼同步方式進行代碼的加鎖操作,同步的現實有兩種方式
同步代碼塊
同步方法
同步代碼塊使用synchronized關鍵字進行同步代碼塊的聲明,但是在使用此操作時必須明確的指出到底要鎖定的是那一個對象,一般都是以當前對象爲主:
Synchronized(對象){ // 一般都是將this進行鎖定
需要同步的代碼
}
package com.cym.syn;
class MyTickerTreadimplements Runnable{ // Runnable接口
private int ticker = 5; // 一共才5張票
public void run() { // 覆寫run()方法
for (int i = 0; i < 100; i++) { // 表示循環10次
synchronized(this){ // 使用同步代碼塊
if(ticker > 0){
try {
Thread.sleep(300); //延遲
} catch (InterruptedException e) {
// TODO Auto-generated catchblock
e.printStackTrace();
} // 延遲
System.out.println("賣票:ticker = " + this.ticker--);
}
}
}
}
}
public class Main {
public static void main(String[]args) {
MyTickerTread mt = new MyTickerTread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
此時解決了同步的問題,但是在java中也可以通過同步方法解決這樣的問題.
package com.cym.syn;
class MyTickerTreadimplements Runnable{ // Runnable接口
private int ticker = 5; // 一共才5張票
public void run() { // 覆寫run()方法
for (int i = 0; i < 100; i++) { // 表示循環10次
sale();
}
}
public synchronized void sale(){
if(ticker > 0){
try {
Thread.sleep(300); //延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 延遲
System.out.println("賣票:ticker = " + this.ticker--);
}
}
}
public class Main {
public static void main(String[]args) {
MyTickerTread mt = new MyTickerTread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
此時,實際上就可以給出java中方法定義的完整格式了:
[訪問權限
[static][final][synchronized][abstract][native]
[返回值類型
方法名稱(參數列表)throws 異常1,異常2....{}
發現同步可以保證資源的完整性,但是過多的同步也會出現問題。
死鎖在程序中過多的同步會產生死鎖的問題,那麼死鎖屬於程序運行的時候發生的一種特殊的狀態,本章只是演示一個簡單的操作代碼,只要觀察到死鎖最終的運行狀態即可
package com.cym.syn;
class Robber {
public synchronized void say(Casualtyc) {
System.out.println("搶劫");
c.give();
}
public synchronized void give() {
System.out.println("搶劫成功");
}
}
class Casualty {
public synchronized void say(Robber r){
System.out.println("不給");
r.give();
}
public synchronized void give() {
System.out.println("報警成功");
}
}
public class DeadLockDemo implements Runnable {
private Robber r = new Robber();
private Casualty c = new Casualty();
public DeadLockDemo() {
new Thread(this).start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catchblock
e.printStackTrace();
}
c.say(r);
}
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catchblock
e.printStackTrace();
}
r.say(c);
}
public static void main(String[]args) {
new DeadLockDemo();
}
}
在本塊中只需要記住一個概念即可:
多個線程共享同一資源的時候需要進行同步,但是過多的同步有可能會造成死鎖。
解決辦法有:以上講過的(同步,設置優先級)
生存者和消費者在多線程中有一個最經典的操作案例 ----- 生存者和消費者,生存者不斷生產內容,但是消費者不斷取出內容。
基本實現現在假設生產的內容都保存在info類中,則在生產者要有info類的引用,而消費者中也要存在info類的引用。
生產者應該不斷的生產信息、消費者不斷的取出,所以實現多線程的操作。
從代碼中可以發現一下幾點問題:
生成的內容有可能發生不一致的內容。
出現了重複取值和重複設置的問題
加入同步爲了保證程序操作數據的完整性,此時,最好加入同步的操作,可以直接在info類中定義一個同步方法,專門完成設置和取得的內容。
此時可以保證數據的一致性,但是依然存在重取和重複設置的問題。那麼該如何去掉這些問題?
Object對線程的支持
Notify()和notifyAll()
對於喚醒的操作有兩個:notify()、notifyAll()。一般來說,所以等待的線程會按照順序進行排列,如果現在使用了notufy()方法的話,則會喚醒第一個等待的線程執行,而如果使用了notifyAll()方法,則會喚醒所有的等待線程,那個線程的優先級高,那個線程就有可能先執行。
現在就利用Object以上方法解決線程的等待喚醒操作
爲什麼使用多線程?1.耗時的操作使用線程,提高應用程序響應
2.並行操作時使用線程,如C/S架構的服務器端併發線程響應用戶的請求。
3.多CPU系統中,使用線程提高CPU利用率
4.改善程序結構。一個既長又複雜的進程可以考慮分爲多個線程,成爲幾個獨立或半獨
立的運行部分,這樣的程序會利於理解和修改。
線程的生命週期
1.線程的生命週期
線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
(1)生命週期的五種狀態
新建(new Thread)
當創建Thread類的一個實例(對象)時,此線程進入新建狀態(未被啓動)。
例如:Thread t1=new Thread();
就緒(runnable)
線程已經被啓動,正在等待被分配給CPU時間片,也就是說此時線程正在就緒隊列中排隊等候得到CPU資源。例如:t1.start();
運行(running)
線程獲得CPU資源正在執行任務(run()方法),此時除非此線程自動放棄CPU資源或者有優先級更高的線程進入,線程將一直運行到結束。
堵塞(blocked)
由於某種原因導致正在運行的線程讓出CPU並暫停自己的執行,即進入堵塞狀態。
正在睡眠:用sleep(long t) 方法可使線程進入睡眠方式。一個睡眠着的線程在指定的時間過去可進入就緒狀態。
正在等待:調用wait()方法。(調用motify()方法回到就緒狀態)
死亡(dead)
當線程執行完畢或被其它線程殺死,線程就進入死亡狀態,這時線程不可能再進入就緒狀態等待執行。
自然終止:正常運行run()方法後終止
異常終止:調用stop()方法讓一個線程終止運行
線程的優缺點多線程的優點很明顯,線程中的處理程序依然是順序執行,符合普通人的思維習慣,所以編程簡單。但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔(電腦程序開很多配置如果不好,電腦會很卡)。並且線程間的共享變量可能造成死鎖的出現。(死鎖以上說過)