JAVA多線程下高並發的處理經驗


java中的線程:java中,每個線程都有一個調用棧存放在線程棧之中,一個java應用總是從main()函數開始運行,被稱為主線程。一旦創建一個新的線程,就會產生一個線程棧。線程總體分為:用戶線程和守護線程,當所有用戶線程執行完畢的時候,JVM自動關閉。
但是守候線程卻不獨立於JVM,守候線程一般是由操作系統或者用戶自己創建的。 線程的生命周期:當一個線程被創建之后,進入新建狀態,JVM則給他分配內存空間,並進行初始化操作。當線程對象調用了start()方法,該線程就處於就緒狀態(可執行狀態),JVM會為其創建方法調用棧、和程序計數器,處於可執行狀態下的線程隨時可以被cpu調度執行。
CPU執行該線程的時候,該線程進入執行狀態。執行過程中,該線程遇倒像wait()等待阻塞、以及synchronized鎖同步阻塞或者調用線程的sleep()方法等進入一個阻塞狀態,阻塞之后通過notify()或者notifyAll()方法喚醒重新獲取對象鎖之后再行進入就緒狀態,
等待cpu執行進去執行狀態、當線程執行完或者return則線程正常結束,如果發生處理的運行時異常,則線程因為異常而結束。這是一個線程的整個運行的生命周期。如下圖所示:

 

 


線程的幾種實現:

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多線程並發的業務場景:在互聯網的大環境下很多場景都對並發要求越來越高,像天貓雙十一秒殺、春運火車票搶票、微信搶紅包、以及一些業務對某種資源的請求數量的控制、以及一些業務需要整個系統的輸入輸出順序一致性等問題,這些都需要考慮到並發導致的數據安全性問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM