Java多線程的概念及創建方法
一、首先我們需要明白幾個概念:程序、進程和線程
程序:指令集,是一個靜態概念,比如說桌面上的一個應用就是一個程序。不管它運不運行都是一個程序
進程:操作系統調度程序,是一個動態概念。還是拿上面那個例子,當我們點擊運行的時候,操作系統開始調度,這就啓動了一個進程。
線程:線程是程序運行 的最小單位。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。(百度上copy過來的,哈哈),它和進程的關係是在一個進程內可以有多個線程,實際上它就是程序運行之後,進程執行的多條路徑。多線程就是指一個進程可以有多條執行路徑。比如說:一個人在桌子上吃飯,這是單進程單線程。多個人在一桌吃飯,這是單進程多線程。簡而言之就是,有多個人同時使用同一個資源,這就是多線程。多線程涉及到的主要有併發、死鎖、線程安全等問題。接下來就來具體說一下多線程是怎麼實現的。
二、線程實現的幾種方法
在Java裏面實現線程的方法主要由三種:
1、繼承Thread類+實現run()方法
貼上代碼:
/**
* 創建多線程,繼承Thread,重寫run方法
*/
public class ThreadPra extends Thread {
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println("跑了"+i +"步");
}
}
public static void main(String[] args) {
// 新生線程
Thread t1=new Thread();
Thread t2= new Thread();
// 啓動線程
t1.start();
t2.start();
}
}
2、實現Runnable接口+run方法
public class Ticket implements Runnable {
private int num= 50;
@Override
public void run() {
while (true){
if (num<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"搶到了"+num--);
}
}
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t,"路人甲");
Thread t2= new Thread(t,"黃牛");
Thread t3 = new Thread(t,"程序員");//線程新生
t1.start();//線程就緒
t2.start();
t3.start();
}
}
運行結果:
可以看到這個搶佔資源的順序是隨機的。誰搶到了資源誰就先運行。
由於繼承Java的單繼承,因此推薦使用Runnable接口,優點1避免單繼承和2便於資源共享
啓動:使用靜態代理
1)創建真實角色2)創建代理角色Thread+引用
3)代理角色.start()
缺點:不能拋出異常,沒有返回值!!
這樣的實現是不是看起來很簡單!!!
3、實現Callable接口,重寫call方法,用Future獲取返回值
使用這種方式。我們需要藉助任務調度線程池ScheduledThreadPoolExecutor
,比如我們如果需要運行兩個線程,我們可以用:
ExecutorService exe = new ScheduledThreadPoolExecutor(2);
指定線程的個數爲2,
然後創建新的線程:(這裏的Race實現了Callable接口,重寫了call方法)
Race l =new Race("tortoise",1000); Race r = new Race("rabbit",500);通過result.get()獲取到返回值
// 獲取返回值 Future<Integer> res1 = exe.submit(l); Future<Integer> res2 = exe.submit(r);
獲取返回值,最後停止服務
exe.shutdownNow();
完整的代碼段:
ExecutorService exe = new ScheduledThreadPoolExecutor(2); Race l =new Race("tortoise",1000); Race r = new Race("rabbit",500); // 獲取返回值 Future<Integer> res1 = exe.submit(l); Future<Integer> res2 = exe.submit(r); Thread.sleep(4000);//跑4秒就停 l.setFlag(false); r.setFlag(false); System.out.println("烏龜跑了"+ res1.get()+"步"); System.out.println("兔子跑了"+ res2.get()+"步"); // 停止服務 exe.shutdownNow();
這種方式的優點是:1)可以對外聲明異常2)有返回值。缺點是比較繁瑣
三、線程的狀態
簡單來說線程一個有五個主要的狀態:新生、就緒、運行、阻塞、死亡
那麼這幾個狀態之間怎麼轉換?以下圖來說明,注意:阻塞解除後會進入就緒狀態
1、新生狀態(new )
2、就緒狀態(線程準備就緒,待CPU分配時間片即可運行)
3、CPU給了時間片,線程進入運行狀態
4、阻塞狀態 如sleep或者等待I/O設備等資源
5、死亡狀態(兩種情況:一種是正常死亡,線程體正常執行完畢。一種是外部干涉,調用stop或者destroy強行終止,不會釋放鎖)
涉及到同步的具體的幾種狀態:
這裏的重點是怎麼樣創建線程和終止線程。創建線程前面已經說過了。這裏我們看一下終止線程的幾種方法。
第一種是線程運行完畢,自然終止(死亡)。
第二種是外部干涉。外部干涉也有幾種方式。
1)線程類中定義線程體使用的標識如flag標識 2)線程體使用該標識 3)提供對外的方法改變該標識 4)外部根據條件調用該方法即可
線程阻塞的方法:1)join合併線程 2)yield暫停當前正在執行的線程對象,它是一個靜態方法,並執行其他線程 3)sleep休眠,這個是用的比較多的一個方法。調用這個方法後,線程進入休眠狀態,不會釋放鎖,此時若有新的線程則會進行等待。
sleep方法示例:
public class sleepDemo { public static void main(String[] args) throws InterruptedException { Date endTime = new Date(System.currentTimeMillis()+10*1000); long end =endTime.getTime(); while (true){ // 輸出 System.out.println(new SimpleDateFormat("hh:mm:ss").format(endTime)); // 休眠 Thread.sleep(1000); // 構建下一秒的時間 endTime = new Date(endTime.getTime()+1000); if(endTime.getTime() -10000> end){ break; } } } }
其他的線程的信息:
1、Thread.currentThread獲得當前線程。2、獲取名稱、設置名稱、優先級、判斷狀態
proxy.setPriority(Thread.MIN_PRIORITY);
proxy.isAlive()
四、線程同步(併發)
同步(併發):多個線程訪問同一個資源時,我們要確保這份資源的安全性。比如說銀行取錢的時候,多個人操作相同的賬戶,存取錢。如果處理不好就可能會發生不安全的問題。爲了避免這種問題,我們需要採取相應的措施。
解決併發問題所採取的主要由幾種方法:
1、給線程加鎖。java裏面採取使用synchronized關鍵字的方式。synchronized關鍵字 ->同步
Java裏面同步有同步塊和同步方法
同步塊:
synchronize(引用類型|this類.class){
//方法體
}
同步方法:
在方法中加入synchronized關鍵字,保證線程安全。
難點:線程範圍不能太大,否則浪費資源,造成等待,也不能過小,否則造成線程不安全
五、死鎖
死鎖是由什麼引起的呢?
過多的併發,造成資源等待,比如我們操作系統書裏面提到的筷子問題,每個人都在等待旁邊的人釋放筷子。這就造成了無限等待,也就造成了死鎖問題。這個時候我們就需要採取相應的措施。
解決死鎖問題的兩個個典型的方法是生產者---消費者模式、管程法
生產者消費者模式:也稱有限緩衝問題,讓生產者在緩衝區滿時休眠,等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒。
代碼:
場景:比如我們定義一個場景Movie,Player和Watch都共同使用這個資源,這就涉及到了同步問題。
生產者使用資源Movie:
/** * 生產者 */ public class Player implements Runnable{ private Movie m; public Player(Movie m){ super(); this.m=m; } @Override public void run() { for(int i= 0;i<10 ;i++){ if(0 == i%2){ m.play("左"); }else{ m.play("右"); } } } }
消費者(也使用Movie):
/** * 消費者 */ public class Watch implements Runnable { private Movie m; public Watch(Movie m){ super(); this.m=m; } @Override public void run() { for(int i= 0;i<10 ;i++){ m.watch(); } } }
定義兩個類共用movie資源,我們定義flag信號燈。
/** * 一個場景,共同的資源 * 生產者消費者模式,信號燈法 * wait() */ public class Movie { private String pic; // 信號燈 // flag -->T生產者生產,消費者等待,生產完成後通知消費者 // flag -->F 消費者消費,生產者等待,消費完成後通知生產者 private boolean flag = true; public synchronized void play(String pic){ if (!flag){//生產者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 開始生成 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 生產完畢 System.out.println("生產了:"+pic); this.pic = pic; // 通知消費 this.notify(); // 生產者停下 this.flag = false; } public synchronized void watch(){ if(flag){//消費者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 開始消費 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 消費完畢 System.out.println("消費了了:"+pic); // 通知生產 this.notifyAll(); // 消費者停下 this.flag = true; } }
main方法:
public class App { public static void main(String[] args) { Movie m =new Movie(); // 多線程 Player p = new Player(m); Watch w = new Watch(m); // 訪問同一份資源 new Thread(p).start(); new Thread(w).start(); } }
結果:
這裏如果不使用信號燈的話,輸出的結果是一樣的。
注意:wait方法和notify和同步一起使用的!!
六、任務調度
最後提一下Timer定時器
public class TimerDemo1{ public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("哈哈...."); } },new Date(System.currentTimeMillis()+1000), 2000); } }
我們可以用Timer來實現任務定時,比如說上面的例子,1秒之後每隔2秒打印“哈哈....”同樣還可以定其他的時間。
!!!好了,以上就是我對線程的一些相關的理解和梳理,重點就是Java中線程的創建、終止、還有線程的幾種狀態。。具體項目中用到的線程肯定不止這麼簡單,會涉及到線程池,任務調度相關的東西。未完待續。。。。