多執行緒技術講解---持續更新
多執行緒技術講解
一、傳統執行緒技術的回顧
1. 多執行緒的建立(兩種)
- 通過建立執行緒物件,複寫run(),通過start()呼叫該執行緒。
本質是繼承
Thread thread = new Thread(){
@Override
public void run() {...}
};
thread.start();
- 通過有參構造建立執行緒物件,複寫run(),通過start()呼叫該執行緒。
本質是介面實現Runnable
Thread thread2 = new Thread(new Runnable() { @Override public void run() {...} }); thread2.start();
- 原始碼
- 首先進入構造方法中看見init方法
通過對成員變數的target的賦值,就能實現多執行緒。
2. 執行緒池的創立
傳統的執行緒建立,壽命只有從執行緒物件的確定,以及到執行緒程式碼的結束,如果一個程式需要不同的時間,建立大量不同的執行緒,用上述的兩種方式建立執行緒,太過於消耗記憶體資源。由此引入新的的技術
- 執行緒池技術
執行緒池的創立都是通過Executors建立,通過代理實現不同型別執行緒池的建立,
執行緒池都是通過不同的佇列的實現,(LinkedBlockingQueue,SynchronousQueue)
// 固定執行緒數 ExecutorService threadPool = Executors.newFixedThreadPool(3); // 根據任務排程執行緒數(首選,當它回收舊執行緒時,停止建立新執行緒,在使用出問題時,才使用newFixedThreadPool) ExecutorService threadPool = Executors.newCachedThreadPool(); // 單執行緒池像是newFixedThreadPool(1);在處理單執行緒同步問題的時候,可以減少對同步複雜性,因為只有一個執行緒執行,就不存在同步的可能 ExecutorService threadPool = Executors.newSingleThreadExecutor();
常用的API
- execute(Runnable command ) 執行緒呼叫執行緒池資源的核心
- Thread.CurrentThread()獲得當前執行緒物件
- shutdownNow()
- shutdown();
例子
ExecutorService threadPool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int task = i;
threadPool.execute(new Runnable() {
@Override
public void run() { }
}
});
}
System.out.println("shutdown");
threadPool.shutdownNow();
2.1 從任務中的返回值
- Runnable是執行工作的獨立任務,但是它不返回值,如希望在任務完成時能返回一個值,那麼可以實現Callable介面而不是Runnable介面。Callable是一種具有型別引數的泛型,它的型別引數表示的是從call()而不是run()中返回的值,並且使用ExecutorService.submit()方法呼叫它
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
public String call(){
return "result of TaskWithResult"+id;
}
}
public class CallableDemo{
public static void main(String[] args){
ExecutorService exec = Executors.newCacheThreadPool();
List<Future<String>> result = new ArrayList<>();
for(int i = 0;i<10;i++){
result.add(exec.submit(new TaskWithResult(1)));
}
for(Future<String> fs : result){
try{
System.out.println(fs.get());
}catch(Exception e){
e.printStackTrace();
}finally{
exec.shutdown();
}
}
}
}
- submit()方法會產生Future物件,它用Callable返回結果的特定型別進行了引數化,可以用isDone()查詢Future是否完成,若完成,那麼就有一個結果,通過get()獲取,若未完成,直接用get()方法獲取,則會阻塞,直至結果就緒。 一般先用isDone()判斷是否完成,再用get。
二、共享受限資源
1、同步技術
- 解決共享資源的競爭
- 在《java程式設計思想》中是這樣描述這個問題的,在就餐時,當叉子快要碰到最後一片食物時,突然食物消失了。是因為另外一個去食用了這片食物。這就是併發,所以在程式設計過程中,要避免出現這種情況
- 解決方法:防止這種衝突的方法就是當資源被一個任務使用的時候,在其上加鎖,第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在被解鎖之前,就無法訪問它。
基本上所有的併發模式在解決執行緒衝突問題的時候,都是採用序列化訪問共享資源的方案,意思是在給定時間內,只允許一個任務訪問共享資源。通常做法是在程式碼前面加上一條鎖語句,使得在一段時間內,只有一個任務可以執行這段程式碼,因為鎖的語句產生了一種相互排斥的效果,所以這樣機制常常稱為互斥量(mutex)
方案1、synchronized關鍵字
- java提供了關鍵字synchronized的形式,防止資源衝突提供了內建支援,當任務要執行synchronized關鍵字保護的程式碼片段的時候,它將檢驗鎖是否可用,然後獲取鎖,執行程式碼,釋放鎖。
- 注意:
1、要控制對共享資源的訪問,得先把它保證進一個物件,然後把所有要訪問這個資源的方法標記為synchronized。
2、 對類的資料成員,都宣告為private,而且只能通過方法來訪問這些資料。然後對方法進行synchronized標記,這樣就可以實現對資料訪問的同步
3、對於某個特定的物件來說,其所有synchronized方法共享同一把鎖,這可以被用來防止多個任務同時訪問被編碼為物件記憶體
1.1 臨界區
- 有時,只是希望防止多個執行緒同時訪問方法內部的部分程式碼而不是防止訪問整個方法.通過這樣子方式分離出來的程式碼段被稱為臨界區(critical section),它也使用synchronized關鍵字建立。在這種方式被稱為同步程式碼塊
–示例
static class Outputer{
//程式碼內加鎖
public void output(String name) {
int len = name.length();
//同步鎖,必須是同一個物件,對有併發問題的程式碼進行同步
//同步程式碼塊
synchronized (this) {
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println();
}
}
//方法上加鎖,預設檢測的是this,就是該類的物件。
public synchronized void output2(String name) {
int len = name.length();
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println();
}
// 檢測的是該類的位元組碼物件,如果要和output同步,把this改為Outputer.class
public static synchronized void output3(String name) {
int len = name.length();
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println();
}
}
方案2、Lock物件
- 在java.util.concurrent類庫中還包含有定義在java.util.concurrent.locks中的顯示互斥機制。Lock物件必須顯式的建立,鎖定和釋放。和內建的鎖相比,程式碼缺乏優雅性,但是對於解決某些問題,更加靈活。
- 關鍵API
//建立鎖物件
Lock lock = new ReentrantLock();
lock.lock();//上鎖
try{
...同步程式碼...
}finally{
lock.unlock();//釋放鎖
}
注意(重點):鎖是必須釋放的,意味著,需要由finally關鍵字,在finally程式碼塊中釋放鎖。不然一旦程式有異常,那麼鎖資源得不到釋放,所有需要該鎖的執行緒都不能執行
方案3、原子操作
在前面的兩種方案中,都是通過鎖進行了同步,但是出現同步的原因還有可能是原子問題。
比如在jvm中對long和double變數(64位)的讀取和寫入操作,是當成2個32位操作進行的。這樣對long和double型變數的操作就不是原子性的,可能會進行上下文的切換,導致任務結果的不正確。
- 解決方案:在定義long和double變數時,如果使用volatile關鍵字。就會獲得原子性。
- 這就是原子操作可以由執行緒機制來保證其不可中斷。
3.1原子類
java SE5 引入了AtomicInteger,AtomicLong,AtomicReference等特殊的原子性變數類,主要是用於效能調優,去掉同步鎖
public class AtomicityTest implements Runnable {
// private int i = 0;
private AtomicInteger i = new AtomicInteger(0);
public int getValue(){
//return i;
return i.get();
}
private /*synchronized*/ void evenIncrement(){
/*i++;
* i++;
* */
i.addAndGet(2);
}
@Override
public void run() {
while (true){
evenIncrement();
}
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Aborting");
System.exit(0);
}
},5000);
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while (true){
int val = at.getValue();
if(val%2!=0){
System.out.println(val);
System.exit(0);
}
}
}
}
方案4、執行緒本地儲存
- 防止任務在共享資源上產生衝突的第二種方法就是根除對變數的共享。
- 執行緒本地儲存是一種自動化機制。可以為使用相同變數的每個不同的執行緒都建立不同的儲存。因此,如果你有五個執行緒都要使用變數X表示的物件。那執行緒本地儲存就會生成5個用於X的不同儲存塊,每個執行緒的資料不共享。
- 重要API
- get();訪問物件內容
- set();設定ThreadLocal的變數
// 單例設計模式
class MyThreadScopeData{
private MyThreadScopeData(){}
private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal();
public static MyThreadScopeData getInstance(){
MyThreadScopeData instance = map.get();
if (instance == null) {
instance = new MyThreadScopeData();
map.set(instance);
}
return instance;
}
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ThreadScopeShareData {
private static ThreadLocal<Integer> x = new ThreadLocal<>();
private static ThreadLocal<MyThreadScopeData> myThreadscopeData = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + "has put data :" + data);
x.set(data);
MyThreadScopeData.getInstance().setName("name"+data);
MyThreadScopeData.getInstance().setAge(data);
new A().get();
new B().get();
}
}).start();
}
}
static class A {
public void get() {
int data = x.get();
System.out.println("A from " + Thread.currentThread().getName() + " has put data :" + data);
MyThreadScopeData mydata = MyThreadScopeData.getInstance();
System.out.println("A from mydata = " + mydata.getName() + " my age " + mydata.getAge()*10);
}
}
static class B {
public void get() {
int data = x.get();
System.out.println("B from" + Thread.currentThread().getName() + "has put data :" + data);
MyThreadScopeData mydata = MyThreadScopeData.getInstance();
System.out.println("A from mydata = " + mydata.getName() + " my age " + mydata.getAge()/10);
}
}
}
在上述示例中,主執行緒,AB執行緒各種獨立,儘管都是從一個數據源獲取資料,但是可以自己對資料進行操作,而不造成相互影響。
-------------------------------10.10 11:57-----------------------------------分割線-------------------------
三、執行緒中斷
3.1 執行緒狀態
- 新建(new):當執行緒被建立時,只有短暫時間會處於該狀態,此時它已經分配了必需的系統資源,並執行了初始化。此刻該執行緒已經有資格獲得cpu時間了,之後排程器把這個執行緒轉變為可執行狀態或阻塞狀態。
- 就緒(Runnable):在這種狀態下,只要排程器把時間片分配給執行緒,執行緒就可以執行。也就是說,在任意時刻,執行緒可以執行也可以不執行。只要排程器能分配時間片給執行緒,它就可以執行;這不同於死亡和阻塞狀態
- 阻塞(Blocked):執行緒能夠執行,但有某個條件阻止它的執行。當執行緒處於阻塞狀態時,排程器將忽略執行緒,不會分配給執行緒任何cpu時間,直到執行緒重新進入就緒狀態,它才可能執行操作。
- 死亡(Dead):處於死亡或終止狀態的執行緒將不再是可排程的,並且再也不會得到cpu時間,它的任務已結束,或不再是可執行的。任務死亡的通常方式是從run()方法返回,但是任務的執行緒還可以被中斷。
進入阻塞狀態的原因
- 通過sleep使任務進入休眠狀態,在這種情況下,任務在指定時間內不會執行。
- 通過wait()使執行緒掛起。直到執行緒得到了notify()和notifyAll()訊息(或者再javaSE5的java.util.concurrent類庫中等價的signal()或signAll()訊息),執行緒才會進入就緒狀態,
- 任務在等待某個輸入和輸出
- 任務試圖在某個物件上呼叫其某個同步控制方法,但是物件鎖不可用,因為另一個任務已經獲取了該鎖
中斷
有時候,對於處於阻塞狀態的任務,需要對其進行終止,那麼就需要強制這個任務跳出阻塞狀態。
- Thread類包含interrupt()方法
- Executor上呼叫shutdownNow()方法
執行緒通訊
前面學習的是,如何使執行緒互不干擾,通過使用鎖技術(互斥)來同步兩個任務的行為,從而使得一個任務不會干涉另外一個任務的資源。
下一步學習,多執行緒之間如何相互合作,以使得多個任務可以一起工作去解決某個問題。
- 任務協作時,關鍵問題是這些任務之間的握手,為了實現這種握手,我們使用了相同的基礎特性:互斥。