幾乎全部的操做系統都支持同時運行多個任務,一個任務一般就是一個程序,每一個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每一個順序執行流就是一個線程。java
幾乎全部的操做系統都支持進程的概念,全部運行中的任務一般對應一個進程( Process)。當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程當中的程序,而且具備必定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。數據庫
一、獨立性:進程是系統中獨立存在的實體,它能夠擁有本身獨立的資源,每個進程都擁有本身私有的地址空間。在沒有通過進程自己容許的狀況下,一個用戶進程不能夠直接訪問其餘進程的地址空間編程
二、動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念。進程具備本身的生命週期和各類不一樣的狀態,這些概念在程序中都是不具有的緩存
三、併發性:多個進程能夠在單個處理器上併發執行,多個進程之間不會互相影響。安全
線程與進程類似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程當中能夠產生多個線程。與進程不一樣的是同類的 多個線程共享同一塊內存空間和一組系統資源,因此係統在產生一個線程,或是在各個線程之間做切換工做時,負擔要比進程小得多,也正由於如此, 線程也被稱爲輕量級進程。
併發:同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行多線程
並行:同一時刻,有多條指令在多個處理器上同時執行 併發
多線程就是幾乎同時執行多個線程(一個處理器在某一個時間點上永遠都只能是一個線程!即便這個處理器是多核的,除非有多個處理器才能實現多個線程同時運行。)。幾乎同時是由於實際上多線程程序中的多個線程其實是一個線程執行一會而後其餘的線程再執行,並非不少書籍所謂的同時執行。
一、進程之間不能共享內存,但線程之間共享內存很是容易。ide
二、系統建立進程時須要爲該進程從新分配系統資源,但建立線程則代價小得多,所以使用多線程來實現多任務併發比多進程的效率高性能
三、Java語言內置了多線程功能支持,而不是單純地做爲底層操做系統的調度方式,從而簡化了Java的多線程編程測試
(1)、繼承Thread類:
第一步:定義Thread類的之類,並重寫run方法,該run方法的方法體就表明了線程須要執行的任務
第二步:建立Thread類的實例
第三步:調用線程的start()方法來啓動線程
public class FirstThread extends Thread { private int i; public void run() { for(;i<100;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { //調用Thread的currentThread方法獲取當前線程 System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { new FirstThread().start(); new FirstThread().start(); } } } }
(2)、實現Runnable接口:
第一步:定義Runnable接口的實現類,並重寫該接口的run方法,該run方法一樣是線程須要執行的任務
第二步:建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正的線程對象
public class SecondThread implements Runnable { private int i; @Override public void run() { for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { SecondThread s1=new SecondThread(); new Thread(s1,"新線程1").start();; new Thread(s1,"新線程2").start(); } } } }
(3)、使用Callable和Future建立線程
細心的讀者會發現,上面建立線程的兩種方法。繼承Thread和實現Runnable接口中的run都是沒有返回值的。因而從Java5開始,Java提供了Callable接口,該接口是Runnable接口的加強版。Callable接口提供了一個call()方法能夠做爲線程執行體,但call()方法比run()方法功能更強大。
建立並啓動有返回值的線程的步驟以下:
第一步:建立 Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,且該call()方法有返回值,再建立 Callable實現類的實例。從Java8開始,能夠直接使用 Lambda表達式建立 Callable對象
第二步:使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call方法的返回值
第三步:使用FutureTask對象做爲Thread對象的target建立並啓動新線程
第四步:經過FutureTask的get()方法得到子線程執行結束後的返回值
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class ThirdThread { public static void main(String[] args) { //ThirdThread rt=new ThirdThread(); FutureTask<Integer> task=new FutureTask<Integer>((Callable<Integer>)()->{ int i=0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+"的循環變量i"+i); } return i; }) ; for(int i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()+"的循環變量i爲"+i); if(i==20) { new Thread(task,"有返回值的線程").start();; } } try { System.out.println("子線程的返回值"+task.get()); }catch(Exception e) { e.printStackTrace(); } } }
採用Runnable、Callable接口的方式建立多線程的優缺點:
優勢:
一、線程類只是實現了 Runnable接口或 Callable接口,還能夠繼承其餘類
二、在這種方式下,多個線程能夠共享同一個 target對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將CPU、代碼和數據分開,造成清晰的模型,較好地體現了面向對象的思想。
缺點:
編程稍稍複雜,若是須要訪問當前線程,則必須使用Thread.currentThread()方法。
採用繼承 Thread類的方式建立多線程的優缺點:
優勢:
編寫簡單,若是須要訪問當前線程,則無須使用 Thread.current Thread()方法,直接使用this便可得到當前線程
缺點:
由於線程已經繼承了Thread類,因此不能再繼承其餘類
新建和就緒狀態:
當程序使用new關鍵字建立一個線程後,該線程就處於新建狀態。
當線程對象調用了start()方法後,該線程就處於就緒狀態。
運行和阻塞狀態:
若是處於就緒狀態的線程獲取了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。
當線程調用sleep(),調用一個阻塞式IO方法,線程會被阻塞
死亡狀態:
一、run()或者call()方法執行完成,線程正常結束
二、線程拋出一個未捕獲的Exception或Error
三、直接調用該線程的stop方法來結束該線程——該方法容易致使死鎖,不推薦使用
線程狀態轉化圖
Thread提供了讓一個線程等待另外一個線程完成的方法——join方法。當在某個程序執行流中調用其直到被 join方法加入的join線程執行完爲止
public class JoinThread extends Thread { //提供一個有參數的構造器,用於設置該線程的名字 public JoinThread(String name) { super(name); } //重寫run方法,定義線程體 public void run() { for(int i=0;i<10;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) throws InterruptedException { //啓動子線程 new JoinThread("新線程").start(); for(int i=0;i<10;i++) { if(i==5) { JoinThread jt=new JoinThread("被join的線程"); jt.start(); //main線程調用了jt線程的join方法,main線程 //必須等jt執行結束纔會向下執行 jt.join(); } System.out.println(Thread.currentThread().getName()+" "+i); } } }
運行結果:
main 0 main 1 main 2 main 3 main 4 新線程 0 新線程 1 新線程 2 新線程 3 被join的線程 0 新線程 4 被join的線程 1 新線程 5 被join的線程 2 新線程 6 被join的線程 3 新線程 7 被join的線程 4 新線程 8 被join的線程 5 新線程 9 被join的線程 6 被join的線程 7 被join的線程 8 被join的線程 9 main 5 main 6 main 7 main 8 main 9
有一種線程,它是在後臺運行的,它的任務是爲其餘的線程提供服務,這種線程被稱爲「後臺線程( Daemon Thread)」,又稱爲「守護線程」或「精靈線程」。JVM的垃圾回收線程就是典型的後臺線程。
後臺線程有個特徵:若是全部的前臺線程都死亡,後臺線程會自動死亡。
調用 Thread對象的 setDaemon(true)方法可將指定線程設置成後臺線程。下面程序將執行線程設置成後臺線程,能夠看到當全部的前臺線程死亡時,後臺線程隨之死亡。當整個虛擬機中只剩下後臺線程時,程序就沒有繼續運行的必要了,因此虛擬機也就退出了。
public class DaemonThread extends Thread { //定義後臺線程的線程體與普通線程沒有什麼區別 public void run() { for(int i=0;i<1000;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { DaemonThread t=new DaemonThread(); //將此線程設置爲後臺線程 t.setDaemon(true); t.start(); for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } //程序到此執行結束,前臺線程(main)結束,後臺線程也隨之結束 } }
運行結果:
main 0 Thread-0 0 main 1 Thread-0 1 Thread-0 2 main 2 Thread-0 3 Thread-0 4 Thread-0 5 main 3 main 4 Thread-0 6 main 5 Thread-0 7 Thread-0 8 main 6 main 7 main 8 Thread-0 9 main 9 Thread-0 10 Thread-0 11 Thread-0 12 Thread-0 13 Thread-0 14 Thread-0 15 Thread-0 16 Thread-0 17 Thread-0 18 Thread-0 19 Thread-0 20 Thread-0 21
若是須要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則能夠經過調用 Thread類的靜態 sleep方法來實現。 sleep方法有兩種重載形式
static void sleep(long millis):讓當前正在執行的線程暫停millis毫秒,並進入阻塞狀態
static void sleep(long millis,int nanos):讓當前正在執行的線程暫停millis毫秒加上nanos毫微秒,並進入阻塞狀態,一般咱們不會精確到毫微秒,因此該方法不經常使用
import java.util.Date; public class SleepTest { public static void main(String[] args) throws InterruptedException { for(int i=0;i<10;i++) { System.out.println("當前時間"+new Date()); Thread.sleep(1000); } } }
每一個線程執行時都有必定的優先級,優先級高的線程得到較多的執行機會,優先級低的線程則得到較少的執行機會。
每一個線程默認的優先級都與建立它的父線程的優先級相同,在默認狀況下,main線程具備普通優先級,由main線程建立的子線程也具備普通優先級。
Thread類提供了 setPriority(int newPriority)、 getPriority()方法來設置和返回指定線程的優先級,其中 setPriority()方法的參數能夠是一個整數,範圍是1-10之間,也可使用 Thread類的以下三個靜態常量
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值時1
NORM_PRIPRITY:其值是5
public class PriorityTest extends Thread { //定義一個構造器,用於建立線程時傳入線程的名稱 public PriorityTest(String name) { super(name); } public void run() { for(int i=0;i<50;i++) { System.out.println(getName()+",其優先級是:"+getPriority()+"循環變量的值:"+i); } } public static void main(String[] args) { //改變主線程的優先級 Thread.currentThread().setPriority(6); for(int i=0;i<30;i++) { if(i==10) { PriorityTest low=new PriorityTest("低級"); low.start(); System.out.println("建立之初的優先級:"+low.getPriority()); //設置該線程爲最低優先級 low.setPriority(Thread.MIN_PRIORITY); } if(i==20) { PriorityTest high=new PriorityTest("高級"); high.start(); System.out.println("建立之初的優先級"+high.getPriority()); high.setPriority(Thread.MAX_PRIORITY); } } } }
現有以下代碼:
public class Account { private String accountNo; private double balance; public Account() {} public Account(String accountNo, double balance) { super(); this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this==obj) { return true; } if(obj!=null&&obj.getClass()==Account.class) { Account target=(Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
import com.alibaba.util.Account; public class DrawThread extends Thread{ //模擬用戶帳戶 private Account account; //當前取錢線程所但願的錢數 private double drawAmount; public DrawThread(String name,Account account,double drawAmount) { super(name); this.account=account; this.drawAmount=drawAmount; } //多個線程修改同一個共享數據,可能發生線程安全問題 @Override public void run() { if(account.getBalance()>drawAmount) { System.out.println(getName()+"取錢成功"+" "+drawAmount); try { Thread.sleep(1); }catch(Exception e) { e.printStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t餘額爲"+" "+account.getBalance()); }else { System.out.println("餘額不足,取錢失敗"); } } }
import com.alibaba.util.Account; public class DrawTest { public static void main(String[] args) { Account account=new Account("1234567",1000); //模擬兩個線程同時操做帳號 new DrawThread("甲", account, 800).start();; new DrawThread("乙", account, 800).start();; } }
如今咱們來分析一下以上代碼:
咱們如今但願實現的操做是模擬多個用戶同時從銀行帳戶裏面取錢,若是用戶取錢數小於等於當前帳戶餘額,則提示取款成功,並將餘額減去取款錢數,若是餘額不足,則提示餘額不足,取款失敗。
Account 類:銀行帳戶類,裏面有一些帳戶的基本信息,以及操做帳戶信息的方法
DrawThread類:繼承了Thread,是一個多線程類,用於模擬多個用戶操做同一個帳戶的信息
DrawTest:測試類
這時咱們運行程序可能會看到以下運行結果:
甲取錢成功 800.0 乙取錢成功 800.0 餘額爲 200.0 餘額爲 -600.0
餘額居然爲-600,餘額不足也能取出錢來,這就是線程安全問題。由於線程調度的不肯定性,出現了偶然的錯誤。
①、同步代碼塊:
爲了解決線程問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式以下:
synchronized(obj){
//此處的代碼就是同步代碼塊
}
咱們將上面銀行中DrawThread類做以下修改:
import com.alibaba.util.Account; public class DrawThread extends Thread{ //模擬用戶帳戶 private Account account; //當前取錢線程所但願的錢數 private double drawAmount; public DrawThread(String name,Account account,double drawAmount) { super(name); this.account=account; this.drawAmount=drawAmount; } //多個線程修改同一個共享數據,可能發生線程安全問題 @Override public void run() { //使用account做爲同步監視器,任何線程在進入下面同步代碼塊以前 //必須先得到account帳戶的鎖定,其餘線程沒法得到鎖,也就沒法修改它 //這種作法符合:"加鎖-修改-釋放鎖"的邏輯 synchronized(account) { if(account.getBalance()>drawAmount) { System.out.println(getName()+"取錢成功"+" "+drawAmount); try { Thread.sleep(1); }catch(Exception e) { e.printStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t餘額爲"+" "+account.getBalance()); }else { System.out.println("餘額不足,取錢失敗"); } } } }
咱們來看此次的運行結果:
甲取錢成功 800.0 餘額爲 200.0 餘額不足,取錢失敗
咱們發現結果變了,是咱們但願看到的結果。由於咱們在可能發生線程安全問題的地方加上了synchronized代碼塊
②:同步方法:
與同步代碼塊對應,Java的多線程安全支持還提供了同步方法,同步方法就是使用 synchronized關鍵字來修飾某個方法,則該方法稱爲同步方法。對於 synchronized修飾的實例方法(非 static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。同步方法語法格式以下:
public synchronized void 方法名(){
//具體代碼
}
③、同步鎖:
從Java5開始,Java提供了一種功能更強大的線程同步機制—一經過顯式定義同步鎖對象來實現同步,在這種機制下,同步鎖由Lock對象充當。
Lock提供了比 synchronized方法和 synchronized代碼塊更普遍的鎖定操做,Lock容許實現更靈活的結構,能夠具備差異很大的屬性,而且支持多個相關的 Condition對象。
在實現線程安全的控制中,比較經常使用的是 ReentrantLock(可重入鎖)。使用該Lock對象能夠顯式加鎖、釋放鎖,一般使用ReentrantLock的代碼格式以下:
class X{ //定義鎖對象 private final ReentrantLock lock=new ReentrantLock(); //... //定義須要保護線程安全的方法 public void m() { //加鎖 lock.lock(); try { //須要保證線程安全的代碼 //...method body }finally { //釋放鎖 lock.unlock(); } } }
死鎖:
當兩個線程相互等待對方釋放同步監視器時就會發生死鎖,Java虛擬機沒有監測,也沒有采起措施來處理死鎖狀況,因此多線程編程時應該採起措施避免死鎖岀現。一旦岀現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是全部線程處於阻塞狀態,沒法繼續。
死鎖是很容易發生的,尤爲在系統中出現多個同步監視器的狀況下,以下程序將會出現死鎖
class A{ public synchronized void foo(B b) { System.out.println("當前線程名:"+Thread.currentThread().getName()+"進入A實例的foo方法");//① try { Thread.sleep(200); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("當前線程名:"+Thread.currentThread().getName()+"企圖調用B的方法");//③ b.last(); } public synchronized void last() { System.out.println("進入了A類的last方法"); } } class B{ public synchronized void bar(A a) { System.out.println("當前線程名:"+Thread.currentThread().getName()+"進入B實例的bar方法");//② try { Thread.sleep(200); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("當前線程名:"+Thread.currentThread().getName()+"企圖調用A的方法");//④ a.last(); } public synchronized void last() { System.out.println("進入了B類的last方法"); } } public class DeadLock implements Runnable { A a=new A(); B b=new B(); public void init() { Thread.currentThread().setName("主線程"); a.foo(b); System.out.println("進入了主線程以後"); } @Override public void run() { Thread.currentThread().setName("副線程"); b.bar(a); System.out.println("進入副線程以後"); } public static void main(String[] args) { DeadLock d=new DeadLock(); new Thread(d).start(); d.init(); } }
運行結果:
從圖中能夠看出,程序既沒法向下執行,也不會拋出任何異常,就一直「僵持」着。究其緣由,是由於:上面程序中A對象和B對象的方法都是同步方法,也就是A對象和B對象都是同步鎖。程序中兩個線程執行,副線程的線程執行體是 DeadLock類的run()方法,主線程的線程執行體是 Deadlock的main()方法(主線程調用了init()方法)。其中run()方法中讓B對象調用b進入foo()方法以前,該線程對A對象加鎖—當程序執行到①號代碼時,主線程暫停200ms:CPU切換到執行另外一個線程,讓B對象執行bar()方法,因此看到副線程開始執行B實例的bar()方法,進入bar()方法以前,該線程對B對象加鎖——當程序執行到②號代碼時,副線程也暫停200ms:接下來主線程會先醒過來,繼續向下執行,直到③號代碼處但願調用B對象的last()方法——執行該方法以前必須先對B對象加鎖,但此時副線程正保持着B對象的鎖,因此主線程阻塞;接下來副線程應該也醒過來了,繼續向下執行,直到④號代碼處但願調用A對象的 last()方法——執行該方法以前必須先對A對象加鎖,但此時主線程沒有釋放對A對象的鎖——至此,就出現了主線程保持着A對象的鎖,等待對B對象加鎖,而副線程保持着B對象的鎖,等待對A對象加鎖,兩個線程互相等待對方先釋放,因此就出現了死鎖。
系統啓動一個新線程的成本是比較高的,由於它涉及與操做系統交互。在這種情形下,使用線程池能夠很好地提升性能,尤爲是當程序中須要建立大量生存期很短暫的線程時,更應該考慮使用線程池。
與數據庫鏈接池相似的是,線程池在系統啓動時即建立大量空閒的線程,程序將一個 Runnable對象或 Callable對象傳給線程池,線程池就會啓動一個空閒的線程來執行它們的run()或call()方法,當run()或call()方法執行結束後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的run()或call()方法。
建立線程池的幾個經常使用的方法:
1.newSingleThreadExecutor
建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
2.newFixedThreadPool
建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
3.newCachedThreadPool
建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,
那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。
4.newScheduledThreadPool
建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolTest { public static void main(String[] args) { ExecutorService pool=Executors.newFixedThreadPool(6); Runnable target=()->{ for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()+"的i的值"+i); } }; pool.submit(target); pool.submit(target); pool.submit(target); //關閉線程池 pool.shutdown(); } }
運行結果:
pool-1-thread-1的i的值0 pool-1-thread-2的i的值0 pool-1-thread-3的i的值0 pool-1-thread-2的i的值1 pool-1-thread-1的i的值1 pool-1-thread-2的i的值2 pool-1-thread-3的i的值1 pool-1-thread-2的i的值3 pool-1-thread-1的i的值2 pool-1-thread-2的i的值4 pool-1-thread-3的i的值2 pool-1-thread-2的i的值5 pool-1-thread-1的i的值3 pool-1-thread-2的i的值6 pool-1-thread-3的i的值3 pool-1-thread-2的i的值7 pool-1-thread-1的i的值4 pool-1-thread-2的i的值8 pool-1-thread-3的i的值4 pool-1-thread-2的i的值9 pool-1-thread-1的i的值5 pool-1-thread-3的i的值5 pool-1-thread-1的i的值6 pool-1-thread-1的i的值7 pool-1-thread-1的i的值8 pool-1-thread-1的i的值9 pool-1-thread-3的i的值6 pool-1-thread-3的i的值7 pool-1-thread-3的i的值8 pool-1-thread-3的i的值9
個人博客即將同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3vtuwevgbfms4