JAVA多執行緒下高併發的處理經驗

lyly4413發表於2019-02-24

java中的執行緒:java中,每個執行緒都有一個呼叫棧存放線上程棧之中,一個java應用總是從main()函式開始執行,被稱為主執行緒。一旦建立一個新的執行緒,就會產生一個執行緒棧。執行緒總體分為:使用者執行緒和守護執行緒,當所有使用者執行緒執行完畢的時候,JVM自動關閉。但是守候執行緒卻不獨立於JVM,守候執行緒一般是由作業系統或者使用者自己建立的。

執行緒的生命週期:當一個執行緒被建立之後,進入新建狀態,JVM則給他分配記憶體空間,並進行初始化操作。當執行緒物件呼叫了start()方法,該執行緒就處於就緒狀態(可執行狀態),JVM會為其建立方法呼叫棧、和程式計數器,處於可執行狀態下的執行緒隨時可以被cpu排程執行。CPU執行該執行緒的時候,該執行緒進入執行狀態。執行過程中,該執行緒遇倒像wait()等待阻塞、以及synchronized鎖同步阻塞或者呼叫執行緒的sleep()方法等進入一個阻塞狀態,阻塞之後通過notify()或者notifyAll()方法喚醒重新獲取物件鎖之後再行進入就緒狀態,等待cpu執行進去執行狀態、當執行緒執行完或者return則執行緒正常結束,如果發生處理的執行時異常,則執行緒因為異常而結束。這是一個執行緒的整個執行的生命週期。如下圖所示:

https://i.iter01.com/images/62c0f8d42e01b39e1b1d972c78cc99aa38e52343a831b92a938faf5f2f0361a2.jpg

執行緒的幾種實現:

1、繼承Thread類,重寫該類的run方法

class MyThread extends Thread {
    
    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        Thread myThread1 = new MyThread(); // 建立一個新的執行緒  myThread1  此執行緒進入新建狀態
        myThread1 .start();  // 呼叫start()方法使得執行緒進入可執行狀態
    }
}

2、實現Runnable介面,並重寫該介面的run()方法,該run()方法同樣是執行緒執行體,建立Runnable實現類的例項,並以此例項作為Thread類的target來建立Thread物件,該Thread物件才是真正的執行緒物件

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        Runnable myRunnable = new MyRunnable(); // 建立一個Runnable實現類的物件

        Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target建立新的執行緒
               
        thread1.start(); // 呼叫start()方法使得執行緒進入就緒狀態
              
        }
    }
}

3、使用Callable和Future介面建立執行緒。具體是建立Callable介面的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的物件,且以此FutureTask物件作為Thread物件的target來建立執行緒

public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 建立MyCallable物件
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable物件

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask物件作為Thread物件的target建立新的執行緒
                thread.start();                      //執行緒進入到就緒狀態
            }
        }

        System.out.println("主執行緒for迴圈執行完畢..");
        
        try {
            int sum = ft.get();            //取得新建立的新執行緒中的call()方法返回的結果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 與run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

繼承Thread和實現Runnable介面實現多執行緒的區別:

繼承Thread類、實現Runnable介面,在程式開發中只要是多執行緒,肯定永遠以實現Runnable介面為主,因為實現Runnable介面相比繼承Thread類有如下優勢:

​ 1、可以避免由於Java的單繼承特性而帶來的侷限;

​ 2、增強程式的健壯性,程式碼能夠被多個執行緒共享,程式碼與資料是獨立的;

​ 3、適合多個相同程式程式碼的執行緒區處理同一資源的情況。

執行緒的優先順序別:

java執行緒可以通過setPriority()方法對其設定一個優先順序別,高優先順序別的執行緒比低優先順序別的執行緒有更高的機率得到先執行,優先順序可以用0到10的整數表示,0為最低優先順序別、10為最高優先順序別。當執行緒排程器決定那個執行緒需要排程時,會根據這個優先順序進行排程選擇;1)Thread類有三個優先順序靜態常量:MAX_PRIORITY為10,為執行緒最高優先順序;MIN_PRIORITY取值為1,為執行緒最低優先順序;NORM_PRIORITY取值為5,為執行緒中間位置的優先順序。預設情況下,執行緒的優先順序為NORM_PRIORITY。2)一般來說優先順序別高的執行緒先獲取cpu資源先執行,但特殊情況下由於現在的計算器都是多核多執行緒的配置,有可能優先順序低的執行緒先執行,具體的執行還是看JVM排程來決定。

幾種執行緒同步的方法:

1、使用synchronized獲取物件互斥鎖:這種方式是最常用也比較安全的一種方式,採用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖叫做互斥鎖。每個物件都有一個monitor(鎖標記),當執行緒擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個物件系統都會為其建立一個互斥鎖,這個鎖是為了分配給執行緒的,防止打斷原子操作。每個物件的鎖只能分配給一個執行緒,因此叫做互斥鎖。我們在使用同步的時候進來爸鎖的粒度控制的精細一點,有時候沒必要鎖整個方法,只需要鎖一個程式碼塊即可達到我們的業務需求,這樣避免其他執行緒阻塞時間過長造成效能上的影響。

package per.thread;
 
import java.io.IOException;
 
public class Test {
    
    private int i = 0;
    private Object object = new Object();
     
    public static void main(String[] args) throws IOException  {
        
    	Test test = new Test();
        
        Test.MyThread thread1 = test.new MyThread();
        Test.MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 
     
     
    class MyThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);
                try {
                    System.out.println("執行緒"+Thread.currentThread().getName()+"進入睡眠狀態");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {
                    // TODO: handle exception
                }
                System.out.println("執行緒"+Thread.currentThread().getName()+"睡眠結束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}

2、使用特殊域變數volatile實現執行緒同步:volatile修飾的變數是一種稍弱的同步機制,因為每個執行緒中的成員變數都會對這個物件的一個私有拷貝,每個執行緒獲取的資料都是從私有拷貝記憶體中獲取,而volatile修飾之後代表這個變數只能從共享記憶體中獲取,禁止私有拷貝。在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。從記憶體的可見性上來看,寫入volatile變數相當於退出同步程式碼塊,而讀取volatile變數相當於進入同步程式碼塊。但程式碼中過度依賴於volatile變數來控制同步狀態,往往比使用鎖更加不安全,使用同步機制會更安全一些。當且僅當滿足以下所有條件時,才應該使用volatile變數: 1、對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個執行緒更新變數的值。 2、該變數沒有包含在具有其他變數的不變式中。

 class Bank {
            //需要同步的變數加上volatile
            private volatile int account = 100;
 
            public int getAccount() {
                return account;
            }
            //這裡不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }

3、使用重入鎖Lock實現執行緒同步: 在jdk1.5以後java.util.concurrent.locks包下提供了這一種方式來實現同步訪問。因為synchronized同步之後會存在一個阻塞的過程,如果這個阻塞的時間過久,嚴重影響我們程式碼的質量以及帶來系統效能上的問題。因為我們需要一種機制,讓等待的執行緒到達一定時間之後能夠響應中斷,這就是Lock的作用。另外lock還可以知道執行緒有沒有成功獲取到物件鎖,synchronized無法做到。Lock比synchronized提供更多的功能。但要注意的是:1)Lock不是Java語言內建的,synchronized是Java語言的關鍵字,因此是內建特性。Lock是一個類,通過這個類可以實現同步訪問;2)Lock和synchronized有一點非常大的不同,採用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

package com.dylan.thread.ch2.c04.task;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * This class simulates a print queue
 *
 */
public class PrintQueue {
 
	/**
	 *  建立一個ReentrantLock例項 
	 */
	private final Lock queueLock=new ReentrantLock();
	
	/**
	 * Method that prints a document
	 * @param document document to print
	 */
	public void printJob(Object document){
        //獲得鎖 
		queueLock.lock();
		
		try {
			Long duration=(long)(Math.random()*10000);
			System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),(duration/1000));
			Thread.sleep(duration);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
            //釋放鎖 
			queueLock.unlock();
		}
	}
}

 注:關於Lock物件和synchronized關鍵字的選擇: 

        a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助使用者處理所有與鎖相關的程式碼。 
        b.如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化程式碼 
        c.如果需要更高階的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally程式碼釋放鎖 

4、使用ThreadLocal管理變數:如果使用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();
            }
        }

5、使用阻塞佇列實現執行緒同步:自從Java 1.5之後,在java.util.concurrent包下提供了若干個阻塞佇列,或Redis訊息佇列等來實現同步等等。。。

java多執行緒併發的業務場景:在網際網路的大環境下很多場景都對併發要求越來越高,像天貓雙十一秒殺、春運火車票搶票、微信搶紅包、以及一些業務對某種資源的請求數量的控制、以及一些業務需要整個系統的輸入輸出順序一致性等問題,這些都需要考慮到併發導致的資料安全性問題。

 

 

 

 

 

 

 

 

相關文章