線程和併發(一)

線程基礎知識

線程基本概念

進程

進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。程序運行時系統就會創建一個進程,併爲它分配資源,然後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷。

Linux系統函數fork()可以在父進程中創建一個子進程,這樣的話,在一個進程接到來自客戶端新的請求時就可以複製出一個子進程讓其來處理,父進程只需負責監控請求的到來,然後創建子進程讓其去處理,這樣就能做到併發處理。

線程

線程是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程都有獨立的運行棧和程序計數器(PC),線程切換開銷小。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。

線程和進程的區別:

  • 進程是資源分配的最小單位,線程是程序執行的最小單位。

  • 進程有自己的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。

  • 線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點。

  • 但是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉並不會對另外一個進程造成影響,因爲進程有自己獨立的地址空間。

併發(Concurrency)和並行(Parallelism)

併發和並行是兩個非常容易被混淆的概念。它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是串行的。而並行是真正意義上的“同時執行”。

多線程在單核CPU的話是順序執行,也就是交替運行(併發)。多核CPU的話,因爲每個CPU有自己的運算器,所以在多個CPU中可以同時運行(並行)。
高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。

臨界區

臨界區用來表示一種公共資源或者說是共享數據,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區資源被佔用,其他線程要想使用這個資源,就必須等待。在並行程序中,臨界區資源是保護的對象。

阻塞和非阻塞

非阻塞指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回,而阻塞與之相反。

Thread和runnable

在java中可有兩種方式實現多線程,一種是繼承Thread類,一種是實現Runnable接口.

通過繼承Thread類代碼示例如下:

package org.thread.demo;  
class MyThread extends Thread{  
private String name;  
public MyThread(String name) {  
super();  
this.name = name;  
}  
public void run(){  
for(int i=0;i<10;i++){  
System.out.println("線程開始:"+this.name+",i="+i);  
}  
}  
}  
package org.thread.demo;  
public class ThreadDemo01 {  
public static void main(String[] args) {  
MyThread mt1=new MyThread("線程a");  
MyThread mt2=new MyThread("線程b");  
mt1.run();  
mt2.run();  
//MyThread mt1=new MyThread("線程a");  
//MyThread mt2=new MyThread("線程b");  
//mt1.start();  
//mt2.start();  
} 
}

通過實例發現啓動線程需要執行start方法。

那麼爲啥非要使用start();方法啓動多線程呢?

在JDK的安裝路徑下,src.zip是全部的java源程序,通過此代碼找到Thread中的start()方法的定義,可以發現此方法中使用了private native void start0();其中native關鍵字表示可以調用操作系統的底層函數,那麼這樣的技術成爲JNI技術(java Native Interface)

通過runnable接口創建線程實例如下:

package org.runnable.demo;  
class MyThread implements Runnable{  
private String name;  
public MyThread(String name) {  
this.name = name;  
}
public void run(){  
for(int i=0;i<100;i++){  
System.out.println("線程開始:"+this.name+",i="+i);  
}  
}  
};

package org.runnable.demo;  
import org.runnable.demo.MyThread;  
public class ThreadDemo01 {  
public static void main(String[] args) {  
MyThread mt1=new MyThread("線程a");  
MyThread mt2=new MyThread("線程b");  
new Thread(mt1).start();  
new Thread(mt2).start();  
}  
}

兩種實現方式的區別和聯繫:

在程序開發中只要是多線程肯定永遠以實現Runnable接口爲主,因爲實現Runnable接口相比繼承Thread類有如下好處:

  • 避免點繼承的侷限,一個類可以繼承多個接口。
  • 適合於資源的共享
  • 線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類

 Callable和Future

Callable

Callable接口代表一段可以調用並返回結果的代碼;Future接口表示異步任務,是還沒有完成的任務給出的未來結果。所以說Callable用於產生結果,Future用於獲取結果。

一般情況下是配合ExecutorService來使用的,在ExecutorService接口中聲明瞭若干個submit方法的重載版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

Future

Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning爲true還是false,肯定返回true。

isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。

isDone方法表示任務是否已經完成,若任務完成,則返回true;

get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;

get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

 因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了下面的FutureTask。

 FutureTask

FutureTask實現了RunnableFuture接口,這個接口的定義如下:

public interface RunnableFuture<V> extends Runnable, Future<V> {  
    void run();  
}

可以看到這個接口實現了Runnable和Future接口,接口中的具體實現由FutureTask來實現。這個類的兩個構造方法如下 :

public FutureTask(Callable<V> callable) {  
        if (callable == null)  
            throw new NullPointerException();  
        sync = new Sync(callable);  
    }  
    public FutureTask(Runnable runnable, V result) {  
        sync = new Sync(Executors.callable(runnable, result));  
    }  

簡單的通過callable和future創建線程:

  1. 創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值。
  2. 創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
  3. 使用FutureTask對象作爲Thread對象的target創建並啓動新線程。
  4. 調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值。
public class CreateThreadByCallable {
    public static void main(String[] args) throws Exception{
        FutureTask<Boolean> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println("MyCallable is running....");
        return true;
    }
}

 Thread一些常用方法

  • currentThread():返回對當前正在執行的線程對象的引用。
  • getId(): 返回此線程的標識符
  • getName():返回此線程的名稱
  • getPriority():返回此線程的優先級
  • isAlive():測試這個線程是否還處於活動狀態。(線程處於正在運行或準備運行的狀態)
  • sleep(long millis):使當前正在執行的線程以指定的毫秒數“休眠”(暫時停止執行),具體取決於系統定時器和調度程序的精度和準確性。
  • interrupt():中斷這個線程。
  • interrupted():測試當前線程是否已經是中斷狀態,執行後具有將狀態標誌清除爲false的功能
  • isInterrupted(): 測試線程Thread對相關是否已經是中斷狀態,但不清除狀態標誌
  • isDaemon():測試這個線程是否是守護線程。

如何停止一個線程

stop(),suspend(),resume()(僅用於與suspend()一起使用)這些方法已被棄用。

一個簡單的方式是使用interupt 配合interupted,return,實例如下:

public class MyThread extends Thread {

	@Override
	public void run() {
			while (true) {
				if (this.isInterrupted()) {
					System.out.println("ֹͣ停止了!");
					return;
				}
				System.out.println("timer=" + System.currentTimeMillis());
			}
	}
	public static void main(String[] args) throws InterruptedException {
		MyThread t=new MyThread();
		t.start();
		Thread.sleep(2000);
		t.interrupt();
	}

}

線程的優先級

每個線程都具有各自的優先級,線程的優先級可以在程序中表明該線程的重要性,如果有很多線程處於就緒狀態,系統會根據優先級來決定首先使哪個線程進入運行狀態。但這個並不意味着低
優先級的線程得不到運行,而只是它運行的機率比較小,如垃圾回收機制線程的優先級就比較低。所以很多垃圾得不到及時的回收處理。

線程優先級具有繼承特性比如A線程啓動B線程,則B線程的優先級和A是一樣的。

線程優先級具有隨機性也就是說線程優先級高的不一定每一次都先執行完。

Thread類中包含的成員變量代表了線程的某些優先級。如Thread.MIN_PRIORITY(常數1),Thread.NORM_PRIORITY(常數5),
Thread.MAX_PRIORITY(常數10)。其中每個線程的優先級都在Thread.MIN_PRIORITY(常數1) 到Thread.MAX_PRIORITY(常數10) 之間,在默認情況下優先級都是Thread.NORM_PRIORITY(常數5)。

守護線程

用戶線程:運行在前臺,執行具體的任務,如程序的主線程、連接網絡的子線程等都是用戶線程

守護線程:運行在後臺,爲其他前臺線程服務.也可以說守護線程是JVM中非守護線程的 “傭人”

可以通過調用Thead類的setDaemon(true)方法設置當前的線程爲守護線程,但須注意:

  • setDaemon(true)必須在start()方法前執行,否則會拋出IllegalThreadStateException異常。
  • 在守護線程中產生的新線程也是守護線程
  • 不是所有的任務都可以分配給守護線程來執行,比如讀寫操作或者計算邏輯

線程狀態 

  1. 初始(NEW):新創建了一個線程對象,但還沒有調用start()方法。
  2. 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲“運行”。線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在獲得CPU時間片後變爲運行中狀態(running)。所以運行狀態又可人爲分爲就緒狀態和運行中狀態。
  3.  阻塞(BLOCKED):表示線程阻塞於鎖。
  4.  等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
  5. 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。
  6. 終止(TERMINATED):表示該線程已經執行完畢。

初始狀態

實現Runnable接口和繼承Thread可以得到一個線程類,new一個實例出來,線程就進入了初始狀態。

就緒狀態

  • 就緒狀態只是說你資格運行,CPU時間片沒分到你,你就永遠是就緒狀態。
  • 調用線程的start()方法,此線程進入就緒狀態。
  • 當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖,這些線程也將進入就緒狀態。
  • 當前線程時間片用完了,調用當前線程的yield()方法,當前線程進入就緒狀態。
  • 鎖池裏的線程拿到對象鎖後,進入就緒狀態。

運行中狀態

線程調度程序從可運行池中選擇一個線程作爲當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。

阻塞狀態

阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。

等待狀態

處於這種狀態的線程不會被分配CPU執行時間,它們要等待被顯式地喚醒,否則會處於無限期等待的狀態。當調用wait(),join(),LockSupport.lock()方法線程會進入到WAITING狀態。

超時等待

處於這種狀態的線程不會被分配CPU執行時間,不過無須無限期等待被其他線程顯示地喚醒(無需notify等),在達到一定時間後它們會自動喚醒。調用wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超時等待的功能,也就是調用這些方法後線程會進入TIMED_WAITING狀態,當超時等待時間到達後,線程會切換到Runable的狀態,另外當WAITING和TIMED_WAITING狀態時可以通過Object.notify(),Object.notifyAll()方法使線程轉換到Runable狀態。

終止狀態

當線程的run()方法完成時,或者主線程的main()方法完成時,我們就認爲它終止了。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦終止了,就不能復生。在一個終止的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
 

線程操作

  • Thread.sleep(long millis),一定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,但不釋放對象鎖,millis後線程自動甦醒進入就緒狀態。作用:給其它線程執行機會的最佳方式。
  • Thread.yield(),一定是當前線程調用此方法,當前線程放棄獲取的CPU時間片,但不釋放鎖資源,由運行狀態變爲就緒狀態,讓OS再次選擇線程。作用:讓相同優先級的線程輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由用戶指定暫停多長時間。
  • thread.join()/thread.join(long millis),當前線程裏調用其它線程t的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程t執行完畢或者millis時間到,當前線程進入就緒狀態。
  • obj.wait(),當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。
  • obj.notify()喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的所有線程。
  • LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 當前線程進入WAITING/TIMED_WAITING狀態。對比wait方法,不需要獲得鎖就可以讓線程進入WAITING/TIMED_WAITING狀態,需要通過LockSupport.unpark(Thread thread)喚醒。

join

join方法可以看做是線程間協作的一種方式,很多時候,一個線程的輸入可能非常依賴於另一個線程的輸出,這時,我們就可以使用join來在兩個線程之間進行協作通信。

join的含義是等待線程終止,也就是說,threadA線程中如果調用threadB的join方法,則threadA在threadB執行完畢之前會發生阻塞,一直等待threadB執行完畢,threadA纔會繼續向下執行,我們通過一個例子來看一下join的使用方式。

sleep

public static native void sleep(long millis)方法顯然是Thread的靜態方法,很顯然它是讓當前線程按照指定的時間休眠,其休眠時間的精度取決於處理器的計時器和調度器。需要注意的是如果當前線程獲得了鎖,sleep方法並不會失去鎖。sleep方法經常拿來與Object.wait()方法進行比較,這兩個方法也是比較容易混淆的概念。

它們的主要區別如下:

  • sleep()方法是Thread的靜態方法,而wait是Object實例方法。
  • wait()方法必須要在同步方法或者同步塊中調用,也就是必須已經獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放佔有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉對象鎖;
  • sleep()方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,纔會離開等待池,並且再次獲得CPU時間片纔會繼續執行。

yield

public static native void yield();這是一個靜態方法,一旦執行,它會是當前線程讓出CPU,但是,需要注意的是,讓出的CPU並不是代表當前線程不再運行了,如果在下一次競爭中,又獲得了CPU時間片當前線程依然會繼續運行。另外,讓出的時間片只會分配給當前線程相同優先級的線程。

線程間通信

wait/notify

產生原因

當兩個線程之間存在生產和消費者關係,也就是說第一個線程(生產者)做相應的操作然後第二個線程(消費者)感知到了變化又進行相應的操作。比如像下面的whie語句一樣,假設這個value值就是第一個線程操作的結果,doSomething()是第二個線程要做的事,當滿足條件value=desire後才執行doSomething()。

但是這裏有個問題就是:第二個語句不停過通過輪詢機制來檢測判斷條件是否成立。如果輪詢時間的間隔太小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到自己想要的數據。所以這裏就需要我們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾。

下面相關方法:

wait:

該方法用來將當前線程置入休眠狀態,直到在其他線程調用此對象的notify()方法或notifyAll()方法將其喚醒。

在調用wait()之前,線程必須要獲得該對象的對象級別鎖,因此只能在同步方法或同步塊中調用wait()方法。進入wait()方法後,當前線程釋放鎖。在從wait()返回前,線程與其他線程競爭重新獲得鎖。如果調用wait()時,沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,因此,不需要try-catch結構。
notify():

該方法喚醒在此對象監視器上等待的單個線程。如果有多個線程都在此對象上等待,則會隨機選擇喚醒其中一個線程,對其發出通知notify(),並使它等待獲取該對象的對象鎖。注意“等待獲取該對象的對象鎖”,這意味着,即使收到了通知,wait的線程也不會馬上獲取對象鎖,必須等待notify()方法的線程釋放鎖纔可以。和wait()一樣,notify()也要在同步方法/同步代碼塊中調用。

總結兩個方法:wait()使線程停止運行,notify()使停止運行的線程繼續運行。

 

等待隊列

  • 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) 代碼段內。
  • 與等待隊列相關的步驟和圖

注意:

  • 當前線程想調用對象A的同步方法時,發現對象A的鎖被別的線程佔有,此時當前線程進入同步隊列。簡言之,同步隊列裏面放的都是想爭奪對象鎖的線程。
  • 當一個線程1被另外一個線程2喚醒時,1線程進入同步隊列,去爭奪對象鎖。
  • 同步隊列是在同步的環境下才有的概念,一個對象對應一個同步隊列。
  • 線程等待時間到了或被notify/notifyAll喚醒後,會進入同步隊列競爭鎖,如果獲得鎖,進入RUNNABLE狀態,否則進入BLOCKED狀態等待獲取鎖。

實例展示

示例代碼如下:

public class ThreadWait {

    private Object lock;

    public ThreadWait(Object lock) {
        this.lock = lock;
    }

    public void testWait() {
        try {
            synchronized (lock) {
                System.out.println("start wait........");
                lock.wait();
                System.out.println("end wait........");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadNotify {

    private Object lock;

    public ThreadNotify(Object lock) {
        this.lock = lock;
    }

    public void testNotify() {
        try {
            synchronized (lock) {
                System.out.println("start notify........");
                lock.notify();
                System.out.println("end notify........");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadWaitNotifyDemo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread notifyThread = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        waitThread.start();
        /**
         * 保證waitThread一定會先開始啓動
         */
        Thread.sleep(1000);
        notifyThread.start();
    }
}

 執行結果:

start wait........
start notify........
end notify........
end wait........

從上面的例子中,我們可以得知幾點信息:

  • wait()方法可以使調用該線程的方法釋放持有當前對象的鎖,然後從運行狀態退出,進入等待隊列,直到再次被喚醒。
  • notify()方法可以隨機喚醒等待隊列中等待的一個線程,並使得該線程退出等待狀態,進入可運行狀態

wait()與notify()操作會釋放鎖嗎?

public class ThreadWaitNotifyLockDemo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread waitThread2 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        waitThread1.start();
        waitThread2.start();
    }
}

結果:

start wait........
start wait........

 可以看到,我們啓動兩個線程,都去執行線程等待的操作,從執行結果看到,輸出了兩條“start wait”,這個可以說明,wait()操作會釋放掉當前持有對象的鎖,否則第二個線程根本不會進入代碼塊中執行。

wait()操作會釋放其持有的對象鎖,那麼notify()操作是否也是一樣的呢?我們再通過一個例子來實驗一下:

public class ThreadNotify {

    private Object lock;

    public ThreadNotify(Object lock) {
        this.lock = lock;
    }

    public void testNotify() {
        try {
            synchronized (lock) {
                System.out.println("start notify........" + Thread.currentThread().getName());
                lock.notify();
                //線程休息兩秒
                Thread.sleep(2000);
                System.out.println("end notify........" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadWaitNotifyLock2Demo {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread waitThread1 = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        Thread notifyThread1 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        Thread notifyThread2 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        Thread notifyThread3 = new Thread(() -> {
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.testNotify();
        });
        waitThread1.start();
        notifyThread1.start();
        notifyThread2.start();
        notifyThread3.start();
    }
}

執行結果: 

start wait........
start notify........Thread-3
end notify........Thread-3
start notify........Thread-2
end notify........Thread-2
start notify........Thread-1
end notify........Thread-1
end wait........

 這個例子中,我們啓動了四個線程,第一個線程執行等待操作,其他兩個線程執行喚醒操作,從執行結果中可以看到,當第一次notify後,線程休息了2秒,如果notify釋放鎖,那麼在其sleep的時候,必然會有其他線程爭搶到鎖並執行,但是從結果中,可以看到這並沒有發生,由此可以說明notify()操作不會釋放其持有的對象鎖。

interrupt

interrupt()來自於Thread類,用途是中斷線程。如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException。
我們來看一下在執行wait()後進行interrupt的效果:

public class ThreadWaitInterruptDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread waitThread = new Thread(() -> {
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.testWait();
        });
        waitThread.start();
        waitThread.interrupt();
    }
}

結果如下:

start wait........
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at com.xuangy.concurrency.practice.ThreadWait.testWait(ThreadWait.java:18)
    at com.xuangy.concurrency.practice.ThreadWaitInterruptDemo.lambda$main$0(ThreadWaitInterruptDemo.java:11)
    at java.lang.Thread.run(Thread.java:745)

synchronized

synchronized是Java中的關鍵字,是一種同步鎖。在多線程開發中經常會使用到這個關鍵字,其主要作用是可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊,同時保證一個線程操作的數據的變化被其他線程所看到。很久之前很多人都會稱它爲“重量級鎖”。但是,在JavaSE 1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後變得在某些情況下並不是那麼重了。

使用場景

實例方法:

synchronized可以在實例方法中使用,這時候,鎖住的該類的實例對象,即多個線程同時訪問同一個new出來的對象時候,該方法會進行阻塞。

public synchronized void doSomething() {
    .....
}

 

靜態方法:

synchronized也可以在靜態方法中使用,這時候,鎖住的是類對象,與實例方法不同,無論new出多少個對象,多個線程同時訪問的時候,都會進行阻塞,因爲它們同屬於一個類。

public static synchronized void doSomething() {
    .....
}

 代碼塊:

與實例方法一樣,這時候,鎖住的該類的實例對象,即多個線程同時訪問同一個new出來的對象時候,該方法會進行阻塞。

Class對象:

與靜態方法一樣,鎖住的是類對象,與實例方法不同,無論new出多少個對象,多個線程同時訪問的時候,都會進行阻塞,因爲它們同屬於一個類。

synchronized(SynchronizedThreadDemo.class) {

}

任意的實例對象:

與實例對象類似,只不過這裏可以傳入任意的實例對象,多個線程訪問的時候,如果是同一個實例對象,都會進行阻塞。 

實現原理

synchronized的實現是基於對象鎖機制來實現的,這裏先看一個簡單的例子:

public class SynchronizedDemo {

    private static int counter = 0;

    public static void main(String[] agrs) {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        synchronizedDemo.synMethod();
    }

    public void synMethod() {
        synchronized (this) {
            counter++;
        }
        System.out.println(counter);
    }
}

上面的示例中是一段簡單的代碼,其中synMethod()中有synchronized的代碼塊,我們通過javap命令查看其class文件的字節碼:

 

可以看到,執行同步代碼塊後首先要先執行monitorenter指令,退出的時候monitorexit指令。通過分析之後可以看出,使用Synchronized進行同步,其關鍵就是必須要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程能夠獲取到monitor。
在這裏引用The Java® Virtual Machine Specification中對於同步方法和同步代碼塊的實現原理的介紹:

方法級的同步是隱式的。同步方法的常量池中會有一個ACC_SYNCHRONIZED標誌。當某個線程要訪問某個方法的時候,會檢查是否有ACC_SYNCHRONIZED,
如果有設置,則需要先獲得監視器鎖,然後開始執行方法,方法執行之後再釋放監視器鎖。
這時如果其他線程來請求執行方法,會因爲無法獲得監視器鎖而被阻斷住。值得注意的是,如果在方法執行過程中,發生了異常,
並且方法內部並沒有處理該異常,那麼在異常被拋到方法外面之前監視器鎖會被自動釋放。
同步代碼塊使用monitorenter和monitorexit兩個指令實現。可以把執行monitorenter指令理解爲加鎖,
執行monitorexit理解爲釋放鎖。 每個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器爲0,
當一個線程獲得鎖(執行monitorenter)後,該計數器自增變爲 1 ,當同一個線程再次獲得該對象的鎖的時候,
計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。當計數器爲0的時候。鎖將被釋放,其他線程便可以獲得鎖。

這裏關於monitor監視器,我們在深入聊一下,每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。

在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。

若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。

幾種鎖實現

JVM在1.6版本之後,對synchronized進行了優化。 鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。對象的MarkWord變化爲下圖:

 偏向鎖

偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此爲了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因爲這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹爲重量級鎖,而是先升級爲輕量級鎖。下面我們接着瞭解輕量級鎖。
 

輕量級鎖

倘若偏向鎖失敗,虛擬機並不會立即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹爲重量級鎖。

自旋鎖

輕量級鎖失敗後,虛擬機爲了避免線程真實地在操作系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱爲自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級爲重量級鎖了。

重量級鎖

一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

 鎖消除

消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個局部變量,並且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。

鎖的比較

鎖重入

“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。

public class Service {

    synchronized public void service1() {
        System.out.println("service1");
        service2();
    }

    synchronized public void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized public void service3() {
        System.out.println("service3");
    }

}

MyThread.java

public class MyThread extends Thread {
    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }

}

運行結果

 

 

說明當存在父子類繼承關係時,子類是完全可以通過“可重入鎖”調用父類的同步方法。

另外出現異常時,其鎖持有的鎖會自動釋放。

如果父類有一個帶synchronized關鍵字的方法,子類繼承並重寫了這個方法。 
但是同步不能繼承,所以還是需要在子類方法中添加synchronized關鍵字。

volatile

syc是阻塞式同步,在線程競爭激烈的情況下會升級爲重量級鎖。而在Java中還提供了另外一個關鍵字volatile,它可以說是Java虛擬機提供的最輕量級的同步機制.

volatile的主要作用是多處理器多線程場景的開發中保證了共享變量的“可見性”。而其背後的機制還是非常複雜的。

實現原理

volatile 修飾的成員變量在每次被線程訪問時,都強迫從主存(共享內存)中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到主存(共享內存)。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值,這樣也就保證了同步數據的可見性

如果對聲明瞭volatile變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏。

通過這個機制,使用volatile修飾的變量,使得每個線程都能獲得該變量的最新值。

volatie總結

  • volatile具有可見性不具有原子性,同時能防止指令重排序。
  • volatile之所以具有可見性,是因爲底層中的Lock指令,該指令會將當前處理器緩存行的數據直接寫會到系統內存中,且這個寫回內存的操作會使在其他CPU裏緩存了該地址的數據無效。
  • volatile之所以能防止指令重排序,是因爲Java編譯器對於volatile修飾的變量,會插入內存屏障。內存屏障會防止CPU處理指令的時候重排序的問題

JMM簡析

可見性、有序性、一致性

  • 原子性(Atomicity):由 Java 內存模型來直接保證的原子性變量操作包括 read、load、assign、use、store 和 write。大致可以認爲基本數據類型的操作是原子性的。同時 lock 和 unlock 可以保證更大範圍操作的原子性。而 synchronize 同步塊操作的原子性是用更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式操作的。
  • 可見性(Visibility):是指當一個線程修改了共享變量的值,其他線程也能夠立即得知這個通知。主要操作細節就是修改值後將值同步至主內存(volatile 值使用前都會從主內存刷新),除了 volatile 還有 synchronize 和 final 可以保證可見性。同步塊的可見性是由“對一個變量執行 unlock 操作之前,必須先把此變量同步會主內存中( store、write 操作)”這條規則獲得。而 final 可見性是指:被 final 修飾的字段在構造器中一旦完成,並且構造器沒有把 “this” 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就能看見 final 字段的值。
  • 有序性(Ordering):如果在被線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句指“線程內表現爲串行的語義”,後半句是指“指令重排”現象和“工作內存與主內存同步延遲”現象。Java 語言通過 volatile 和 synchronize 兩個關鍵字來保證線程之間操作的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由“一個變量在同一時刻只允許一條線程對其進行 lock 操作”這條規則獲得,這條規則決定了持有同一個鎖的兩個同步塊只能串行的進入。

內存模型

從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在,它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。

舉例:

  • 本地內存A和B有主內存中共享變量x的副本。
  • 假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存A中。
  • 當線程A和線程B需要通信時(如何激發?--隱式),線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。
  • 隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。


從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。

 指令重排序

在Java內存模型中,爲了效率是允許編譯器和處理器對指令進行重排序,即在單線程環境下,如果執行兩條指令的順序不會影響其執行結果,處理器可以對指令進行重排序,當然重排序它不會影響單線程的運行結果,但是對多線程會有影響。

舉個例子:

class SynchronizedExample {
  int a = 0;
  boolean flag = false;
 
  public synchronized void writer() {
    a = 1;
    flag = true;
  }
 
  public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
  }
}

 

 

在順序一致性模型中,所有操作完全按程序的順序串行執行。而在JMM中,臨界區內的代碼可以重排序。 

JMM屬於語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

對於編譯器重排序,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。

對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

把對volatile變量的單個讀/寫,看成是使用同一個監視器鎖對這些單個讀/寫操作做了同步。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。

這意味着即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性。如果是多個volatile操作或類似於volatile++這種複合操作,這些操作整體上不具有原子性。
 

簡而言之,volatile變量自身具有下列特性:

  • 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
  • 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

heppen-before

從JDK5開始,java使用新的JSR -133內存模型。JSR-133提出了happens-before的概念,通過這個概念來闡述操作之間的內存可見性。如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。這裏提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。 與程序員密切相關的happens-before規則如下:

  • 程序順序規則:一個線程中的每個操作,happens- before 於該線程中的任意後續操作。
  • 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens- before B,且B happens- before C,那麼A happens- before C。

注意,兩個操作之間具有happens-before關係,並不意味着前一個操作必須要在後一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。happens- before的定義很微妙,後文會具體說明happens-before爲什麼要這麼定義。

happen-before操作的規則:

  • 同一個線程中的,前面的操作happen-before後續的操作。(即單線程內按代碼順序執行。但是,在不影響在單線程環境執行結果的前提下,編譯器和處理器可以進行重排序,這是合法的。換句話說,這一是規則無法保證編譯重排和指令重排)。
  • 監視器上的解鎖操作 happen-before 其後續的加鎖操作。(Synchronized 規則)
  • 對volatile變量的寫操作 happen-before 後續的讀操作。(volatile 規則)
  • 線程的start() 方法 happen-before 該線程所有的後續操作。(線程啓動規則)
  • 線程所有的操作 happen-before 其他線程在該線程上調用 join 返回成功後的操作。
  • 如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

volatile寫-讀建立的happens before關係

從JSR-133開始,volatile變量的寫-讀可以實現線程之間的通信。

從內存語義的角度來說,volatile與監視器鎖有相同的效果:volatile寫和監視器的釋放有相同的內存語義;volatile讀與監視器的獲取有相同的內存語義

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
 
    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }
 
    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假設線程A執行writer()方法之後,線程B執行reader()方法。根據happens before規則,這個過程建立的happens before 關係可以分爲兩類:

  1. 根據程序次序規則,1 happens before 2; 3 happens before 4。
  2. 根據volatile規則,2 happens before 3。
  3. 根據happens before 的傳遞性規則,1 happens before 4。

上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens before 關係。黑色箭頭表示程序順序規則;橙色箭頭表示volatile規則;藍色箭頭表示組合這些規則後提供的happens before保證。

這裏A線程寫一個volatile變量後,B線程讀同一個volatile變量。A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量後,將立即變得對B線程可見。
 

 volatile寫-讀的內存語義:

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

爲了實現volatile內存語義,JMM會分別限制編譯器重排序和處理器重排序。下面是JMM針對編譯器制定的volatile重排序規則表:

是否能重排序 第二個操作
第一個操作 普通讀/寫 volatile讀 volatile寫
普通讀/寫     NO
volatile讀 NO NO NO
volatile寫   NO NO

 從表中可以看出:

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
     

 

參考文檔:

https://blog.csdn.net/wtopps/article/details/81569040

https://blog.csdn.net/qq_34337272/article/details/79655194

https://blog.csdn.net/vking_wang/article/details/8574376

https://blog.csdn.net/qq_41701956/article/details/81664921

https://www.jianshu.com/p/e34469924714

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章