轉載:https://blog.csdn.net/csdn_aiyang/article/details/65442540java
概述
說到線程,就不得不先說線程和進程的關係,這裏先簡單解釋一下,進程是系統的執行單位,通常一個應用程序便是一個進程,程序啓動時系統默認有一個主線程,便是UI線程,咱們知道不能作耗時任務,不然ANR程序無響應。這時須要藉助子線程實現,即多線程。因爲線程是系統CPU的最小單位,用多線程其實就是爲了更好的利用cpu的資源。
常見多線程方式
一、繼承Thread類,重寫run函數方法:
class xx extends Thread{
public void run(){
Thread.sleep(1000); //線程休眠1000毫秒,sleep使線程進入Block狀態,並釋放資源
}
}
xx.start(); //啓動線程,run函數運行
二、實現Runnable接口,重寫run函數方法:
Runnable run =new Runnable() {
@Override
public void run() {
}
}
三、實現Callable接口,重寫call函數方法:
Callable call =new Callable() {
@Override
public Object call() throws Exception {
return null;
}
}
小結:Callable 與 Runnable 對比。
相同:都是可被其它線程執行的任務。
不一樣:
①Callable規定的方法是call(),而Runnable規定的方法是run().
②Callable的任務執行後可返回值,而Runnable的任務是不能返回值的
③call()方法可拋出異常,而run()方法是不能拋出異常的。
④運行Callable任務可拿到一個Future對象,Future表示異步計算的結果。經過Future對象可瞭解任務執行狀況,可取消任務的執行。
四、HandlerThread:
handlerThread = new HandlerThread("MyNewThread");//自定義線程名稱
handlerThread.start();
mOtherHandler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg){
if (msg.what == 0x124){
try {
Log.d("HandlerThread", Thread.currentThread().getName());
Thread.sleep(5000);//模擬耗時任務
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
HandlerThread的好處是代碼看起來沒前面的版本那麼亂,相對簡潔一點。還有一個好處就是經過handlerThread.quit()或者quitSafely()使線程結束本身的生命週期。
四、AsyncTask:
具體的使用代碼就不貼上來了,能夠去看個人一篇博文。但值得一說的是,上面說過HandlerThread只開一條線程,任務都被阻塞在一個隊列中,那麼就會使阻塞的任務延遲了,而AsyncTask開啓線程的方法asyncTask.execute()默認是也是開啓一個線程和一個隊列的,不過也能夠經過asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0)開啓一個含有5個新線程的線程池,也就是說有個5個隊列了,假如說你執行第6個耗時任務時,除非前面5個都還沒執行完,不然任務是不會阻塞的,這樣就能夠大大減小耗時任務延遲的可能性,這也是它的優勢所在。當你想多個耗時任務併發的執行,那你更應該選擇AsyncTask。
四、IntentService:
最後是IntentService,相信不少人也不陌生,它是Service的子類,用法跟Service也差很少,就是實現的方法名字不同,耗時邏輯應放在onHandleIntent(Intent intent)的方法體裏,它一樣有着退出啓動它的Activity後不會被系統殺死的特色,並且當任務執行完後會自動中止,無須手動去終止它。例如在APP裏咱們要實現一個下載功能,當退出頁面後下載不會被中斷,那麼這時候IntentService就是一個不錯的選擇了。
線程狀態
一、wait()。使一個線程處於等待狀態,而且釋放全部持有對象的lock鎖,直到notify()/notifyAll()被喚醒後放到鎖定池(lock blocked pool ),釋放同步鎖使線程回到可運行狀態(Runnable)。
二、sleep()。使一個線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉Interrupted異常,醒來後進入runnable狀態,等待JVM調度。
三、notify()。使一個等待狀態的線程喚醒,注意並不能確切喚醒等待狀態線程,是由JVM決定且不按優先級。
四、allnotify()。使全部等待狀態的線程喚醒,注意並非給全部線程上鎖,而是讓它們競爭。
五、join()。使一個線程中斷,IO完成會回到Runnable狀態,等待JVM的調度。
六、Synchronized()。使Running狀態的線程加同步鎖使其進入(lock blocked pool ),同步鎖被釋放進入可運行狀態(Runnable)。
注意:當線程在runnable狀態時是處於被調度的線程,此時的調度順序是不必定的。Thread類中的yield方法可讓一個running狀態的線程轉入runnable。
基礎概念
一、 並行。多個cpu實例或多臺機器同時執行一段代碼,是真正的同時。
二、併發。經過cpu調度算法,讓用戶看上去同時執行,實際上從cpu操做層面不是真正的同時。
三、線程安全。指在併發的狀況之下,該代碼通過多線程使用,線程的調度順序不影響任何結果。線程不安全就意味着線程的調度順序會影響最終結果,好比某段代碼不加事務去併發訪問。
四、線程同步。指的是經過人爲的控制和調度,保證共享資源的多線程訪問成爲線程安全,來保證結果的準確。如某段代碼加入@synchronized關鍵字。線程安全的優先級高於性能優化。
五、原子性。一個操做或者一系列操做,要麼所有執行要麼所有不執行。數據庫中的「事物」就是個典型的院子操做。
六、可見性。當一個線程修改了共享屬性的值,其它線程能馬上看到共享屬性值的更改。好比JMM分爲主存和工做內存,共享屬性的修改過程是在主存中讀取並複製到工做內存中,在工做內存中修改完成以後,再刷新主存中的值。若線程A在工做內存中修改完成但還來得及刷新主存中的值,這時線程B訪問該屬性的值還是舊值。這樣可見性就無法保證。
七、有序性。程序運行時代碼邏輯的順序在實際執行中不必定有序,爲了提升性能,編譯器和處理器都會對代碼進行從新排序。前提是,從新排序的結果要和單線程執行程序順序一致。
Synchronized 同步
因爲java的每一個對象都有一個內置鎖,當用此關鍵字修飾方法時, 內置鎖會保護整個方法。在調用該方法前,須要得到內置鎖,不然就處於阻塞狀態。補充: synchronized關鍵字也能夠修飾靜態方法,此時若是調用該靜態方法,將會鎖住整個類。
一、方法同步。給方法增長synchronized修飾符就能夠成爲同步方法,能夠是靜態方法、非靜態方法,但不能是抽象方法、接口方法。小示例:
public synchronized void aMethod() {
// do something
}
public static synchronized void anotherMethod() {
// do something
}
使用詳解:
線程在執行同步方法時是具備排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的全部同步方法都被鎖定了,在此期間,其餘任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程執行完它所調用的同步方法並從中退出,從而致使它釋放了該對象的同步鎖以後。在一個對象被某個線程鎖定以後,其餘線程是能夠訪問這個對象的全部非同步方法的。
二、塊同步。同步塊是經過鎖定一個指定的對象,來對塊中的代碼進行同步;同步方法和同步塊之間的相互制約只限於同一個對象之間,靜態同步方法只受它所屬類的其它靜態同步方法的制約,而跟這個類的實例沒有關係。若是一個對象既有同步方法,又有同步塊,那麼當其中任意一個同步方法或者同步塊被某個線程執行時,這個對象就被鎖定了,其餘線程沒法在此時訪問這個對象的同步方法,也不能執行同步塊。
三、使用方法同步保護共享數據。示例:
public class ThreadTest implements Runnable{
public synchronized void run(){
for(int i=0;i<10;i++) {
System.out.print(" " + i);
}
}
public static void main(String[] args) {
Runnable r1 = new ThreadTest();
Runnable r2 = new ThreadTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}}
示例詳解:
代碼中可見,run()被加上了synchronized 關鍵字,但保護的並非共享數據。由於程序中兩個線程對象 t一、t2 實際上是另外兩個線程對象 r一、r2 的線程,這個聽起來繞,可是一眼你就能看明白;由於不一樣的線程對象的數據是不一樣的,即 r1,r2 有各自的run()方法,因此輸出結果就沒法預知。這時使用 synchronized 關鍵字可讓某個時刻只有一個線程能夠訪問該對象synchronized數據。每一個對象都有一個「鎖標誌」,當這個對象的一個線程訪問這個對象的某個synchronized 數據時,這個對象的全部被synchronized 修飾的數據將被上鎖(由於「鎖標誌」被當前線程拿走了),只有當前線程訪問完它要訪問的synchronized 數據時,當前線程纔會釋放「鎖標誌」,這樣同一個對象的其它線程纔有機會訪問synchronized 數據。
接下來,咱們把 r2 給註釋掉, 即只保留一個 r 對象。以下:
public class ThreadTest implements Runnable{
public synchronized void run(){
for(int i=0;i<10;i++){
System.out.print(" " + i);
}
}
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}}
示例詳解:
若是你運行1000 次這個程序,它的輸出結果也必定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。由於這裏的synchronized 保護的是共享數據。t1,t2 是同一個對象(r)的兩個線程,當其中的一個線程(例如:t1)開始執行run()方法時,因爲run()受synchronized保護,因此同一個對象的其餘線程(t2)沒法訪問synchronized 方法(run 方法)。只有當t1執行完後t2 纔有機會執行。
四、使用塊同步,示例:
public class ThreadTest implements Runnable{
public void run(){
synchronized(this){ //與上面示例不一樣於關鍵字使用
for(int i=0;i<10;i++){
System.out.print(" " + i);
}
}
}
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
示例詳解:
這個與上面示例的運行結果也同樣的。這裏是把保護範圍縮到最小,this 表明 ‘這個對象’ 。沒有必要把整個run()保護起來,run()中的代碼只有一個for循環,因此只要保護for 循環就能夠了。
最後,再看一個示例:
public class ThreadTest implements Runnable{
public void run(){
for(int k=0;k<5;k++){
System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);
}
synchronized(this){
for(int k=0;k<5;k++) {
System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);
}} }
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1_name");
Thread t2 = new Thread(r,"t2_name");
t1.start();
t2.start();
} }
//運行結果:
t1_name : for loop : 0
t1_name : for loop : 1
t1_name : for loop : 2
t2_name : for loop : 0
t1_name : for loop : 3
t2_name : for loop : 1
t1_name : for loop : 4
t2_name : for loop : 2
t1_name : synchronized for loop : 0
t2_name : for loop : 3
t1_name : synchronized for loop : 1
t2_name : for loop : 4
t1_name : synchronized for loop : 2
t1_name : synchronized for loop : 3
t1_name : synchronized for loop : 4
t2_name : synchronized for loop : 0
t2_name : synchronized for loop : 1
t2_name : synchronized for loop : 2
t2_name : synchronized for loop : 3
t2_name : synchronized for loop : 4
示例詳解:
第一個for 循環沒有受synchronized 保護。對於第一個for 循環,t1,t2 能夠同時訪問。運行結果代表t1 執行到了k=2 時,t2 開始執行了。t1 首先執行完了第一個for 循環,此時t2尚未執行完第一個for 循環(t2 剛執行到k=2)。t1 開始執行第二個for 循環,當t1的第二個for 循環執行到k=1 時,t2 的第一個for 循環執行完了。t2 想開始執行第二個for 循環,但因爲t1 首先執行了第二個for 循環,這個對象的鎖標誌天然在t1 手中(synchronized 方法的執行權也就落到了t1 手中),在t1 沒執行完第二個for 循環的時候,它是不會釋放鎖標誌的。因此t2 必須等到t1 執行完第二個for 循環後,它才能夠執行第二個for 循環。
Volatile 同步
a.volatile關鍵字爲域變量的訪問提供了一種免鎖機制
b.使用volatile修飾域至關於告訴虛擬機該域可能會被其餘線程更新
c.所以每次使用該域就要從新計算,而不是使用寄存器中的值
d.volatile不會提供任何原子操做,它也不能用來修飾final類型的變量
例如:
在上面的例子當中,只需在account前面加上volatile修飾,便可實現線程同步。
代碼實例:
//只給出要修改的代碼,其他代碼與上同
class Bank {
//須要同步的變量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//這裏再也不須要synchronized
public void save(int money) {
account += money;
}
}
注:多線程中的非同步問題主要出如今對域的讀寫上,若是讓域自身避免這個問題,則就不須要修改操做該域的方法。 用final域,有鎖保護的域和volatile域能夠避免非同步的問題。
重入鎖同步
在 JavaSE5.0中 新增了一個 java.util.concurrent 包來支持同步。
ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具備相同的基本行爲和語義,而且擴展了其能力。 ReenreantLock類的經常使用方法有:
ReentrantLock() : 建立一個ReentrantLock實例
lock() : 得到鎖
unlock() : 釋放鎖
注:ReentrantLock()還有一個能夠建立公平鎖的構造方法,但因爲能大幅度下降程序運行效率,不推薦使用 例如:
class Bank {
private int account = 100;
//須要聲明這個鎖
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//這裏再也不須要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
注:關於Lock對象和synchronized關鍵字的選擇:
a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,可以幫助用戶處理全部與鎖相關的代碼。
b.若是synchronized關鍵字能知足用戶的需求,就用synchronized,由於它能簡化代碼
c.若是須要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,不然會出現死鎖,一般在finally代碼釋放鎖
局部變量同步
若是使用ThreadLocal管理變量,則每個使用該變量的線程都得到該變量的副本,副本之間相互獨立,這樣每個線程均可以隨意修改本身的變量副本,而不會對其餘線程產生影響。 ThreadLocal 類的經常使用方法:
ThreadLocal() : 建立一個線程本地變量
get() : 返回此線程局部變量的當前線程副本中的值
initialValue() : 返回此線程局部變量的當前線程的"初始值"
set(T value) : 將此線程局部變量的當前線程副本中的值設置爲value
例如:
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.前者採用以"空間換時間"的方法,後者採用以"時間換空間"的方式
阻塞隊列同步
前面同步方式都是在底層實現的線程同步,可是咱們在實際開發當中,應當儘可能遠離底層結構。 使用javaSE5.0版本中新增的java.util.concurrent包將有助於簡化開發。 本小節主要是使用LinkedBlockingQueue<E>來實現線程的同步 LinkedBlockingQueue<E>是一個基於已鏈接節點的,範圍任意的blocking queue。 隊列是先進先出的順序(FIFO),關於隊列之後會詳細講解~LinkedBlockingQueue 類經常使用方法 LinkedBlockingQueue() : 建立一個容量爲Integer.MAX_VALUE的LinkedBlockingQueue put(E e) : 在隊尾添加一個元素,若是隊列滿則阻塞 size() : 返回隊列中的元素個數 take() : 移除並返回隊頭元素,若是隊列空則阻塞代碼實例: 實現商家生產商品和買賣商品的同步
注:BlockingQueue<E>定義了阻塞隊列的經常使用方法,尤爲是三種添加元素的方法,咱們要多加註意,當隊列滿時:
add()方法會拋出異常
offer()方法返回false
put()方法會阻塞
原子變量同步
須要使用線程同步的根本緣由在於對普通變量的操做不是原子的。
那麼什麼是原子操做呢?原子操做就是指將讀取變量值、修改變量值、保存變量值當作一個總體來操做即-這幾種行爲要麼同時完成,要麼都不完成。在java的util.concurrent.atomic包中提供了建立了原子類型變量的工具類,使用該類能夠簡化線程同步。其中AtomicInteger 表能夠用原子方式更新int的值,可用在應用程序中(如以原子方式增長的計數器),但不能用於替換Integer;可擴展Number,容許那些處理機遇數字類的工具和實用工具進行統一訪問。
AtomicInteger類經常使用方法:
AtomicInteger(int initialValue) : 建立具備給定初始值的新的
AtomicIntegeraddAddGet(int dalta) : 以原子方式將給定值與當前值相加
get() : 獲取當前值
代碼實例:
class Bank {
private AtomicInteger account = new AtomicInteger(100);
public AtomicInteger getAccount() {
return account;
}
public void save(int money) {
account.addAndGet(money);
}
}
補充--原子操做主要有:
對於引用變量和大多數原始變量(long和double除外)的讀寫操做;
對於全部使用volatile修飾的變量(包括long和double)的讀寫操做。
另外,可使用線程池進行管理及優化。
個人相關文章推薦連接地址點擊跳轉:線程優化及線程池管理
---------------------
做者:艾陽丶
來源:CSDN
原文:https://blog.csdn.net/csdn_aiyang/article/details/65442540