程序員必會的Java多線程與併發編程

1、線程三大特性

多線程有三大特性:原子性、可見性、有序性

原子性:
即一個操作或者多個操作,要麼全部執行成功,要麼全都不執行。
一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作都必須要具備原子性才能保證轉賬成功,而不會出現一些意外的情況。

可見性:
當多個線程訪問同一個變量時,如果一個線程修改了這個變量的值,其他線程能夠立即看得到修改後的值。
就拿 i=i+1 舉例:若兩個線程在不同的cpu,如果線程1改變了 i 的值,但是沒有及時刷新到主存中,線程2又使用了 i ,那麼這個 i 的值肯定還是之前的,線程2沒有及時看到線程1對變量 i 的修改,這就是可見性問題。

有序性:
程序執行的順序按照代碼的先後順序執行。
一般來說處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。舉個例子如下:

int a = 10;    //語句1
int r = 2;     //語句2
a = a + 3;     //語句3
r = a*a;       //語句4

則因爲重排序,它的執行順序還可能爲: 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因爲這打破了依賴關係。
顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。

2、Java內存模型

Java內存模型(簡稱JMM),也稱爲共享內存模型。JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。
在這裏插入圖片描述
從上圖來看,線程A與線程B之間如果要進行通信的話,必須要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 然後,線程B到主內存中去讀取線程A之前已更新過的共享變量。

下面通過示意圖來說明這兩個步驟:
在這裏插入圖片描述
如上圖所示,本地內存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內存模型簡稱JMM,定義了一個線程對另一個線程可見。共享變量存放在主內存中,每個線程都有自己的本地內存,JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。用於解決當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存而引發的線程安全問題。

3、Volatile

volatile 關鍵字的作用就是讓變量在多個線程之間可見。

下面我們使用代碼來進行演示:

public class ThreadVolatileDemo  extends Thread{
	public  boolean flag = true;
	
	@Override
	public void run() {
		System.out.println("子線程開始執行....");
		while (flag) {
		}
		System.out.println("子線程結束執行....");
	}
	
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	
	public static void main(String[] args) throws InterruptedException {
		ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
		threadVolatileDemo.start();
		Thread.sleep(600);
		threadVolatileDemo.setFlag(false);
		System.out.println(threadVolatileDemo.flag);
	}
}

運行結果:
在這裏插入圖片描述
在上面的代碼中,我們明明已經將結果設置爲fasle,但是爲什麼還一直在運行呢?
原因:因爲線程之間是不可見的,讀取的是本地的副本,沒有及時讀取到主內存中的最新變量值。
解決辦法:使用volatile關鍵字可以解決線程之間的可見性, 強制線程每次讀取該值的時候都去“主內存”中取值,這樣可以保證每次讀取到的都是變量的最新值。

我們只需要在變量前面加上 volatile 就可以使變量在多個線程之間可見,如下所示:

public volatile boolean flag = true;

加上 volatile 之後的運行結果爲:
在這裏插入圖片描述
此時while循環結束,表明已經讀取到了主存中的最新值。

使用 volatile 時,雖然每個線程都會去內存中讀取最新的變量值,但是 volatile 不具備原子性,每個線程讀取變量的時候可能其他線程並沒有執行完畢,因此可能會引發線程安全問題。

下面我們使用代碼來演示一下:

public class VolatileAtomicityTest {
    private static final int THREADS_CONUT = 20;
    public static volatile int count = 0;
    
    public static void increase() {
        count++;
    }
    
   public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                       increase();
                    }
                }
            });
            
            threads[i].start();
        }
        
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        
        System.out.println(count);
    }
}

運行結果如下:
在這裏插入圖片描述
如果 volatile 具備原子性,那麼運行結果應該爲20000。此時的運行結果明顯不對,說明 volatile 不具備原子性。

那麼怎樣解決原子性問題呢?

答案就是使用AtomicInteger原子類

public class AtomicIntegerTest {
    private static final int THREADS_CONUT = 20;
    public static AtomicInteger count = new AtomicInteger(0);
    
    public static void increase() {
        count.incrementAndGet();
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                       increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

此時的運行結果爲:
在這裏插入圖片描述
說明 AtomicInteger 可以解決原子性問題。

volatile 與 synchronized 的區別

  1. volatile 輕量級,只能修飾變量。 synchronized 重量級,不僅可以修飾變量,還可修飾方法;
  2. volatile 只能保證數據的可見性,不能用來同步(即不能保證數據的原子性),因此多個線程併發訪問 volatile 修飾的變量不會阻塞。synchronized 不僅可以保證可見性,而且還可以保證原子性,因爲只有獲得了鎖的線程才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個線程爭搶 synchronized 鎖對象時,會出現阻塞。
  3. 僅僅使用volatile並不能保證線程安全性,而synchronized則可實現線程的安全性。

4、ThreadLocal(本地線程)

Synchronized用於線程間的數據共享,而ThreadLocal 則主要用於線程間的數據隔離。
當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

ThreadLocal底層通過Map集合實現,以線程作爲key,泛型作爲value,可以理解爲線程級別的緩存。每一個線程都會獲得一個單獨的Map。

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:

void set(Object value)          //設置當前線程的線程局部變量的值
public Object get()            //該方法返回當前線程所對應的線程局部變量
public void remove()           //將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度
protected Object initialValue()//返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null

案例:創建三個線程,每個線程生成自己獨立序列號。

class Res {
	// 生成序列號共享變量
	public static Integer count = 0;
	public static ThreadLocal<Integer> threadLocal = 
		new ThreadLocal<Integer>() { //初始化
		protected Integer initialValue() {
			return 0;
		};
	};
	
	public Integer getNum() {
		int count = threadLocal.get() + 1;
		threadLocal.set(count);
		return count;
	}
}

public class ThreadLocaDemo extends Thread {
	private Res res;

	public ThreadLocaDemo(Res res) {
		this.res = res;
	}

	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println(Thread.currentThread().getName() + "---" + "num:" + res.getNum());
		}
	}

	public static void main(String[] args) {
		Res res = new Res();
		ThreadLocaDemo t1 = new ThreadLocaDemo(res);
		ThreadLocaDemo t2 = new ThreadLocaDemo(res);
		ThreadLocaDemo t3 = new ThreadLocaDemo(res);
		t1.start();
		t2.start();
		t3.start();
	}
}

5、線程池

線程池是指在初始化一個多線程應用程序過程中創建一個線程集合,然後在需要執行新的任務時重用這些線程而不是新建一個線程。線程池中線程的數量通常完全取決於可用內存數量和應用程序的需求。線程池中的每個線程都會被分配一個任務,一旦任務完成之後,線程就會回到線程池中並等待下一次分配任務。

線程池的作用:

  1. 線程池改進了一個應用程序的響應時間。由於線程池中的線程已經準備好且等待被分配任務,應用程序可以直接拿來使用而不用新建一個線程;
  2. 線程池節省了CLR 爲每個短生存週期任務創建一個完整的線程的開銷並可以在任務完成後回收資源;
  3. 線程池根據當前在系統中運行的進程來優化線程時間片;
  4. 線程池允許我們開啓多個任務而不用爲每個線程設置屬性;
  5. 線程池允許我們爲正在執行的任務的程序參數傳遞一個包含狀態信息的對象引用;
  6. 線程池可以用來解決處理一個特定請求最大線程數量限制問題。

Java通過 Executors 提供了四種線程池,分別爲:

  1. newCachedThreadPool 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若沒有可回收的線程時,則新建線程;
  2. newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待;
  3. newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行;
  4. newSingleThreadExecutor 創建一個單線程的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序執行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章