黑馬程序員——java基礎——多線程java
------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流! -------程序員
進程:是一個正在執行中的程序。每個進程執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元。
線程:就是進程中的一個獨立的控制單元。線程在控制着進程的執行。一個進程中至少有一個線程。
安全
一個進程至少有一個線程在運行,當一個進程中出現多個線程時,就稱這個應用程序是多線程應用程序,每一個線程在棧區中都有本身的執行空間,本身的方法區、本身的變量。多線程
jvm在啓動的時,首先有一個主線程,負責程序的執行,調用的是main函數。主線程執行的代碼都在main方法中。jvm
當產生垃圾時,收垃圾的動做,是不須要主線程來完成,由於這樣,會出現主線程中的代碼執行會中止,會去運行垃圾回收器代碼,效率較低,因此由單獨一個線程來負責垃圾回收。
擴展:其實更細節說明jvm,jvm啓動不止一個線程,還有負責垃圾回收機制的線程。
ide
隨機性的原理:由於cpu的快速切換形成,哪一個線程獲取到了cpu的執行權,哪一個線程就執行。函數
返回當前線程的名稱:Thread.currentThread().getName()性能
線程的名稱是由:Thread-編號定義的。編號從0開始。this
線程要運行的代碼都統一存放在了run方法中。spa
線程要運行必需要經過類中指定的方法開啓。start方法。(啓動後,就多了一條執行路徑)
start方法:1)、啓動了線程;2)、讓jvm調用了run方法。
建立線程的方式
建立線程共有兩種方式:繼承方式和實現方式
建立線程的第一種方式:繼承Thread,由子類複寫run方法。
步驟:
1,定義類繼承Thread類;
2,目的是複寫run方法,將要讓線程運行的代碼都存儲到run方法中;
3,經過建立Thread類的子類對象,建立線程對象;
4,調用線程的start方法,開啓線程,並執行run方法。
class MyThread extends Thread{ public MyThread() { super(); // TODO Auto-generated constructor stub } public MyThread(String name) { super(name); // TODO Auto-generated constructor stub } public void run() { for(int i =0 ;i<10;i++){ System.out.println("Demo:"+Thread.currentThread().getName()+":"+i); } } } public class ThreadDemo { public static void main(String[] args) { // TODO Auto-generated method stub MyThread demo1 = new MyThread("線程一"); demo1.start(); MyThread demo2 = new MyThread("線程二"); demo2.start(); for(int i = 0;i<10;i++){ System.out.println("main:"+i); } } }
建立線程的第二種方式:實現一個接口Runnable
步驟:
1,定義類實現Runnable接口。
2,覆蓋接口中的run方法(用於封裝線程要運行的代碼)。
3,經過Thread類建立線程對象;
4,將實現了Runnable接口的子類對象做爲實際參數傳遞給Thread類中的構造函數。
爲何要傳遞呢?由於要讓線程對象明確要運行的run方法所屬的對象。
5,調用Thread對象的start方法。開啓線程,並運行Runnable接口子類中的run方法。
class Ticket implements Runnable{ private int tick = 100; public void run() { while(true) { if(tick>0) { System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } } } class TicketDemo { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t);//建立了一個線程; Thread t2 = new Thread(t);//建立了一個線程; t1.start(); t2.start(); } }
爲何要有Runnable接口的出現?
1:經過繼承Thread類的方式,能夠完成多線程的創建。可是這種方式有一個侷限性,若是一個類已經有了本身的父類,就不能夠繼承Thread類,由於java單繼承的侷限性。
但是該類中的還有部分代碼須要被多個線程同時執行。這時怎麼辦呢?
只有對該類進行額外的功能擴展,java就提供了一個接口Runnable。這個接口中定義了run方法,其實run方法的定義就是爲了存儲多線程要運行的代碼。
因此,一般建立線程都用第二種方式。
由於實現Runnable接口能夠避免單繼承的侷限性
2:實際上是將不一樣類中須要被多線程執行的代碼進行抽取。將多線程要運行的代碼的位置單獨定義到接口中。爲其餘類進行功能擴展提供了前提。
因此Thread類在描述線程時,內部定義的run方法,也來自於Runnable接口
實現Runnable接口能夠避免單繼承的侷限性。並且,繼承Thread,是能夠對Thread類中的方法,進行子類複寫的。可是不須要作這個複寫動做的話,只爲定義線程代碼存放位置,實現Runnable接口更方便一些。因此Runnable接口將線程要執行的任務封裝成了對象。
線程狀態:
被建立:start()
運行:具有執行資格,同時具有執行權;
凍結:sleep(time),wait()—notify()喚醒;線程釋放了執行權,同時釋放執行資格;
臨時阻塞狀態:線程具有cpu的執行資格,沒有cpu的執行權;
消亡:stop()
線程安全問題
多線程安全問題的緣由:
經過圖解:發現一個線程在執行多條語句時,並運算同一個數據時,在執行過程當中,其餘線程參與進來,並操做了這個數據。致使到了錯誤數據的產生。
涉及到兩個因素:
1,多個線程在操做共享數據。
2,有多條語句對共享數據進行運算。
緣由:這多條語句,在某一個時刻被一個線程執行時,尚未執行完,就被其餘線程執行了。
解決安全問題的原理:
只要將操做共享數據的語句在某一時段讓一個線程執行完,在執行過程當中,其餘線程不能進來執行就能夠解決這個問題。
在java中對於多線程的安全問題提供了專業的解決方式——synchronized(同步)
這裏也有兩種解決方式,一種是同步代碼塊,還有就是同步函數。都是利用關鍵字synchronized來實現。
a、同步代碼塊
用法:
synchronized(對象)
{須要被同步的代碼}
同步能夠解決安全問題的根本緣由就在那個對象上。其中對象如同鎖。持有鎖的線程能夠在同步中執行。沒有持有鎖的線程即便獲取cpu的執行權,也進不去,由於沒有獲取鎖。
<span style="font-size:14px;">class Ticket implements Runnable { private int tick = 1000; Object obj = new Object(); public void run() { while(true) { synchronized(obj) { if(tick>0) { //try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } } } } class TicketDemo2 { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); } } </span>
b,同步函數
其實就是將同步關鍵字定義在函數上,讓函數具有了同步性。
同步函數是用的哪一個鎖呢?
經過驗證,函數都有本身所屬的對象this,因此同步函數所使用的鎖就是this鎖。
<span style="font-size:14px;">class Ticket implements Runnable { private int tick = 100; Object obj = new Object(); boolean flag = true; public void run() { if(flag) { while(true) { show(); } } } public synchronized void show()//this { if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--); } } } class ThisLockDemo { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); t1.start(); } </span>
當同步函數被static修飾時,這時的同步用的是哪一個鎖呢
靜態函數在加載時所屬於類,這時有可能尚未該類產生的對象,可是該類的字節碼文件加載進內存就已經被封裝成了對象,這個對象就是該類的字節碼文件對象。
因此靜態加載時,只有一個對象存在,那麼靜態同步函數就使用的這個對象。
這個對象就是類名.class
class Single { private static Single s = null; private Single(){} public static Single getInstance() { if(s==null) { synchronized(Single.class) { if(s==null) //--->A; s = new Single(); } } return s; } } class SingleDemo { public static void main(String[] args) { System.out.println("Hello World!"); } }
同步代碼塊和同步函數的區別
同步代碼塊使用的鎖能夠是任意對象。
同步函數使用的鎖是this,靜態同步函數的鎖是該類的字節碼文件對象。
線程同步的利弊
好處:解決了線程安全問題。
弊端:相對下降性能,由於判斷鎖須要消耗資源,產生了死鎖。
定義同步是有前提的:
1,必需要有兩個或者兩個以上的線程,才須要同步。
2,多個線程必須保證使用的是同一個鎖。
同步死鎖
一般只要將同步進行嵌套,就能夠看到現象。同步函數中有同步代碼塊,同步代碼塊中還有同步函數。
public class DeadLockDemo { public static void main(String[] args) { // TODO Auto-generated method stub // TODO Auto-generated method stub Ticket1 ticket1 = new Ticket1(); Thread thread1 = new Thread(ticket1); thread1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } Thread thread2 = new Thread(ticket1); ticket1.flag = false; thread2.start(); } } class Ticket1 implements Runnable{ private static int ticket1 = 10; private Object obj = new Object(); boolean flag = true; @Override public void run() { // TODO Auto-generated method stub if(flag){ while(true){ synchronized (obj) { show(); } } }else{ while(true){ show(); } } } public synchronized void show(){ synchronized (obj) { if(ticket1>0){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+ticket1--); } } } }
線程間通信:
其實就是多個線程在操做同一個資源,可是操做的動做不一樣。
1:將資源封裝成對象。
2:將線程執行的任務(任務其實就是run方法。)也封裝成對象。
<span style="font-size:14px;">class Res { String name; String sex; boolean flag = false; } class Input implements Runnable { private Res r ; Input(Res r) { this.r = r; } public void run() { int x = 0; while(true) { synchronized(r) { if(r.flag) try{r.wait();}catch(Exception e){} if(x==0) { r.name="mike"; r.sex="man"; } else { r.name="麗麗"; r.sex = "女"; } x = (x+1)%2; r.flag = true; r.notify(); } } } } class Output implements Runnable { private Res r ; Output(Res r) { this.r = r; } public void run() { while(true) { synchronized(r) { if(!r.flag) try{r.wait();}catch(Exception e){} System.out.println(r.name+"...."+r.sex); r.flag = false; r.notify(); } } } } class InputOutputDemo { public static void main(String[] args) { Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } } </span>
等待喚醒機制
涉及的方法:
wait:將同步中的線程處於凍結狀態。釋放了執行權,釋放了資格。同時將線程對象存儲到線程池中。
notify:喚醒線程池中某一個等待線程。
notifyAll:喚醒的是線程池中的全部線程
注意:
1:這些方法都須要定義在同步中。
2:由於這些方法必需要標示所屬的鎖。
你要知道 A鎖上的線程被wait了,那這個線程就至關於處於A鎖的線程池中,只能A鎖的notify喚醒。
3:這三個方法都定義在Object類中。爲何操做線程的方法定義在Object類中?
由於這三個方法都須要定義同步內,並標示所屬的同步鎖,既然被鎖調用,而鎖又能夠是任意對象,那麼能被任意對象調用的方法必定定義在Object類中。
wait和sleep區別:
分析這兩個方法:從執行權和鎖上來分析:
wait:能夠指定時間也能夠不指定時間。不指定時間,只能由對應的notify或者notifyAll來喚醒。
sleep:必須指定時間,時間到自動從凍結狀態轉成運行狀態(臨時阻塞狀態)。
wait:線程會釋放執行權,並且線程會釋放鎖。
Sleep:線程會釋放執行權,可是不釋放鎖。