天天看點

深入了解Java多線程與并發程式設計

一、多線程三大特性

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

  1. 原子性(跟資料庫的事務特性中的原子性類似,資料庫的原子性展現是dml語句執行後需要進行送出):

    了解:即一個操作或多個操作,要麼全部執行并且執行的過程中不會被任何因素打斷,要麼都不執行。

    一個很經典的例子就是銀行賬戶轉賬問題:

    比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

    我們操作資料也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運作肯定會出問題,是以也需要我們使用同步synchronized和lock鎖這些東西來確定這個特性了。

    原子性其實就是保證資料一緻、線程安全一部分,

  2. 可見性:

    可見性是與java記憶體模型息息相關的。

    當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

    若兩個線程在不同的cpu,那麼線程1改變了i的值還沒重新整理到主存,線程2又使用了i,那麼這個i值肯定還是之前的,線程1對變量的修改線程2沒有看到,這就是可見性問題。

  3. 有序性:

    了解:程式執行的順序按照代碼的先後順序執行。

    一般來說,處理器為了提高程式運作效率,可能會對輸入代碼進行優化,它不保證程式中各個語句的執行先後順序同代碼中的順序一緻,但是它會保證程式最終執行結果和代碼順序執行的結果是一緻的。

    例如:

    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,因為這打破了依賴關系。

    顯然重排序對單線程運作是不會有任何問題,而多線程就不一定了,是以我們在多線程程式設計時就得考慮這個問題了。

    多線程中保證有序性的方法:join()

二、Java記憶體模型

jvm的記憶體結構為:堆、棧、方法區,不同于java的記憶體模型,Java的記憶體模型是關于多線程相關的。

了解:共享記憶體模型指的是Java記憶體模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主記憶體之間的抽象關系:線程之間的共享變量存儲在主記憶體(main memory)中(局部變量不會存儲在),每個線程都有一個私有的本地記憶體(local memory),本地記憶體中存儲了該線程以讀/寫共享變量的副本。本地記憶體是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬體和編輯器優化。

總結:什麼是Java記憶體模型:java記憶體模型簡稱jmm,定義了一個線程對另一個線程可見。共享變量存放在主記憶體中,每個線程都有自己的本地記憶體,當多個線程同時通路一個資料的時候,可能本地記憶體沒有及時重新整理到主記憶體,是以就會發生線程安全問題。

三、Volatile關鍵字

Volatile關鍵字的作用:變量在多個線程之間可見。

Volatile關鍵字是非原子性的,不能保證資料的原子性,隻是能夠把解決立馬重新整理到主記憶體中,不能解決并發問題。

如果想要保證資料的原子性,解決并發問題,需要使用并發包裡的AutomicInteger原子類。

volatile與synchronized差別:

僅靠volatile不能保證線程的安全性(原子性)。

1.volatile輕量級,隻能修飾變量。synchronized重量級,還可修飾方法。

2.volatile隻能保證資料的可見性,不能用來同步,因為多個線程并發通路volatile修飾的變量不會阻塞。

synchronized不僅保證可見性,而且還保證原子性,因為隻有獲得了鎖的線程才能進入臨界區,進而保證臨界區中的所有語句都全部執行。多個線程争搶synchronized鎖對象時會出現阻塞。

synchronized會把主記憶體中的共享變量鎖住,永遠隻有一個線程操作主記憶體的共享變量。

線程安全性包括兩個友善:1.可見性 2.原子性

僅僅使用volatile不能保證線程安全性,而synchronized則可實作線程的安全性。

代碼實作:

package chauncy.concurrentprogramming;

class ThreadVolatile extends Thread {
	public volatile boolean flag = true;

	@Override
	public void run() {
		System.out.println("子線程開始執行...");
		while (flag) {

		}
		System.out.println("子線程結束執行...");
	}

	public void isRun(boolean flag) {
		this.flag = flag;
	}
}

/**
 * @classDesc: 功能描述(Volatile關鍵字的使用)
 * @author: ChauncyWang
 * @version: 1.0
 */
public class Volatile {
	public static void main(String[] args) throws InterruptedException {
		ThreadVolatile threadVolatile1 = new ThreadVolatile();
		threadVolatile1.start();
		Thread.sleep(300);
		/**
		 * 如果不對變量加Volatile關鍵字,則子線程不會停止運作 原因:線程之間是不可見的,讀取的是副本,沒有及時讀取到主記憶體結果。
		 * 解決辦法:使用Volatile關鍵字解決線程之間的可見性,強制線程每次讀取該值的時候都去“主記憶體”中取值。
		 */
		threadVolatile1.isRun(false);
		System.out.println("flag:" + threadVolatile1.flag);
	}
}
           
package chauncy.concurrentprogramming;

import java.util.concurrent.atomic.AtomicInteger;

class VolatileNoAtomicThread extends Thread {
	// private static volatile int count = 0;
	private static AtomicInteger atomicInteger = new AtomicInteger(0);

	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			// count++;
			atomicInteger.incrementAndGet();// count++
		}
		System.out.println(getName() + "-----" + atomicInteger);
	}
}

/**
 * @classDesc: 功能描述(Volatile修飾不具有原子性(不具有同步性),不能解決線程安全問題)
 * @author: ChauncyWang
 * @version: 1.0
 */
public class VolatileNoAtomic {
	public static void main(String[] args) {
		// 初始化10個線程
		VolatileNoAtomicThread[] volatileNoAtomicThread = new VolatileNoAtomicThread[10];
		for (int i = 0; i < volatileNoAtomicThread.length; i++) {
			// 建立每一個線程
			volatileNoAtomicThread[i] = new VolatileNoAtomicThread();
		}
		for (int i = 0; i < volatileNoAtomicThread.length; i++) {
			// 啟動每一個線程
			volatileNoAtomicThread[i].start();
		}
	}
}
           

四、TreadLocal

  1. 什麼是ThreadLocal?

    ThreadLocal提高一個線程的局部變量,通路某個線程擁有自己局部變量。

    當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,是以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程對應的副本。

    ThreadLocal接口方法有4個:

    void set(Object value)設定目前線程的線程局部變量的值;

    public Object get()該方法傳回目前線程所對應的線程局部變量;

    public void remove()将目前線程局部變量的值删除,目的是為了減少記憶體的占用,該方法是JDK5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量将自動被垃圾回收,是以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快記憶體的回收速度;

    protected Object initialValue()傳回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的。這個方法是一個延遲調用方法,線上程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的預設實作直接傳回一個null。

  2. ThreadLocal底層實作原理:

    ThreadLocal通過Thread.currentThread();擷取目前線程

    操作map集合:ThreadLocalMap

    void set(Object value)就是Map.put(“目前線程”,值);

    public Object get()就是擷取ThreadLocalMap然後操作後傳回。

    代碼實作:

    package chauncy.concurrentprogramming;
    
    class Res {
    	// private int count=0;
    	/*
    	 * 設定本地局部變量,和其他線程局部變量隔離開,互不影響
    	 */
    	private ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
    		protected Integer initialValue() {
    			// 設定目前線程局部變量的初始化值
    			return 0;
    		};
    	};
    
    	/**
    	 * 
    	 * @methodDesc: 功能描述(生成訂單号)
    	 * @author: ChauncyWang
    	 * @param: @return
    	 * @returnType: int
    	 */
    	public Integer getNum() {
    		int count = this.count.get() + 1;
    		this.count.set(count);
    		return count;
    	}
    }
    
    class ThreadLocalDemo extends Thread {
    	private Res res;
    
    	public ThreadLocalDemo(Res res) {
    		this.res = res;
    	}
    
    	@Override
    	public void run() {
    		for (int i = 0; i < 3; i++) {
    			try {
    				Thread.sleep(30);
    			} catch (Exception e) {
    			}
    			System.out.println(getName() + "----i:" + i + ",number:" + res.getNum());
    		}
    	}
    }
    
    /**
     * @classDesc: 功能描述(本地線程的使用:建立三個線程,每個線程生成自己獨立的序列号)
     * @author: ChauncyWang
     * @version: 1.0
     */
    public class ThreadLocalTest {
    	public static void main(String[] args) {
    		Res res = new Res();
    		ThreadLocalDemo t1 = new ThreadLocalDemo(res);
    		ThreadLocalDemo t2 = new ThreadLocalDemo(res);
    		ThreadLocalDemo t3 = new ThreadLocalDemo(res);
    		t1.start();
    		t2.start();
    		t3.start();
    	}
    }
               

五、線程池

  1. 為什麼要使用線程池?

    因為要通過線程池來管理線程,啟動或者停止一個線程非常耗費資源,是以将線程交給線程池來管理能夠節約記憶體。

    一般在企業開發當中我們都使用線程池,通過spring去整合線程池,異步注解。

  2. 什麼是線程池?

    線程池是指在初始化一個多線程應用程式過程中建立一個線程集合,然後在需要執行新的任務時重用這些線程而不是建立一個線程。線程池中線程的數量通常完全取決于可用記憶體數量和應用程式的需求。然而,增加可用線程數量是可能的。線程池中的每個線程都有被配置設定一個任務,一旦任務已經完成了,線程回到池子中并等待下一次配置設定任務。

  3. 線程池作用:

    基于以下幾個原因,在多線程應用程式中使用線程池是必須的:

    1.線程池改進了一個應用程式的相應時間。由于線程池中的線程已經準備好且等待被配置設定任務,應用程式可以直接拿來使用而不用建立一個線程。

    2.線程池節省了CLR為每個短生命周期任務建立一個完整的線程開銷并可以在任務完成後回收資源。

    3.線程池根據目前在系統中運作的程序來優化線程時間片。

    4.線程池允許我們開啟多個任務而不用為每個線程設定屬性。

    5.線程池允許我們為正在執行任務的程式參數傳遞一個包含狀态資訊的對象引用。

    6.線程池可以用來解決處理一個特定請求最大線程數量限制問題。

  4. 線程池四種建立方式:

    java通過Executors(jdk1.5的并發包)提供四種線程池,分别為:

    1.newCachedThreadPool 建立一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則建立線程。

    2.newFixedThreadPool 建立一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。

    3.newScheduledThreadPool 建立一個定時線程池,支援定時及周期性任務執行

    4.newSingleThreadExecutor 建立一個單線程化的線程池,它隻會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO,LIFO,優先級)執行。(一般不會使用)

    總結:newCachedThreadPool 建立的線程,線程池為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次建立線程。newFixedThreadPool 每次執行傳入參數大小個線程,其他線程在等待(企業中用的不多)。newScheduledThreadPool 使用schedule方法建立機關時間的延遲線程池。

    代碼實作:

    package chauncy.concurrentprogramming.executors;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class NewCachedThreadPool {
    	public static void main(String[] args) {
    		// 建立可緩存線程池
    		ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    		// 執行execute方法表示建立了一個線程,類似于start
    		for (int i = 0; i < 30; i++) {
    			int index = i;
    			// index++;
    			newCachedThreadPool.execute(new Runnable() {
    
    				@Override
    				public void run() {
    					try {
    						Thread.sleep(300);
    					} catch (InterruptedException e) {
    					}
    					// 内部類中使用的i必須是final,但是換成index後就不報錯,因為jdk1.8進行了優化,能識别index是否被改變,如果把int
    					// index=i;下邊的index++放開就會報錯。
    					System.out.println(Thread.currentThread().getName() + "----" + index);
    				}
    			});
    		}
    		// 關閉線程池
    		newCachedThreadPool.shutdown();
    	}
    }
               
    package chauncy.concurrentprogramming.executors;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class NewFixedThreadPool {
    	public static void main(String[] args) {
    		// newFixedThreadPool 每次最多隻能執行三個,其他線程等待執行。
    		ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
    		for (int i = 0; i < 10; i++) {
    			int index = i;
    			newFixedThreadPool.execute(new Runnable() {
    				public void run() {
    					try {
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    					}
    					System.out.println(Thread.currentThread().getName() + "----i:" + index);
    				}
    			});
    		}
    	}
    }
               
    package chauncy.concurrentprogramming.executors;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class NewScheduledThreadPool {
    	public static void main(String[] args) {
    		// 入參為線程池大小,
    		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
    		// schedule執行定時任務線程池,第一個參數需要建立Runnable接口對象,第二、三個參數表示多少個機關時間執行run方法。
    		newScheduledThreadPool.schedule(new Runnable() {
    			public void run() {
    				System.out.println("我是三秒鐘之後執行。。。。");
    			}
    		}, 3, TimeUnit.SECONDS);
    	}
    }
               
    package chauncy.concurrentprogramming.executors;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class NewSingleThreadExecutor {
    	public static void main(String[] args) {
    		ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
    		for (int i = 0; i < 10; i++) {
    			int index = i;
    			newSingleThreadExecutor.execute(new Runnable() {
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + "----i:" + index);
    				}
    			});
    		}
    	}
    }