Java的多線程和並發庫(上)


  一、多線程基礎知識--傳統線程機制的回顧

    1、傳統使用類Thread和接口Runnable實現

      1):在Thread子類覆蓋的run方法中編寫運行代碼

          

      2):在傳遞給Thread對象的Runnable對象的run方法中編寫代碼

          

      3):總結

          查看Thread類的run()方法的源代碼,可以看到其實這兩種方式都是在調用Thread對象的run方法,如果Thread的run方法沒有被覆蓋,並且為該Thread對象設置了一個Runnable對象,該run方法會調用Runnable對象的run方法。

          

  2、定時器Timer和TimerTask

    Timer在實際開發中應用場景不多,一般來說都會用其他第三方庫來實現。但有時會在一些面試題中出現。

    1):模擬雙重定時器

      

  3、線程互斥與同步

    在引入多線程后,由於線程執行的異步性,會給系統造成混亂,特別是在急用臨界資源是,如多個線程急用同一台打印機,會是打印結果交織在一起,難於區分。當多個線程急用共享變量,表格,鏈表時,可能會導致數據處理出錯,因此線程同步的主要內容是使並發執行的各線程之間能夠有效的共享資源和互相合作,從而使程序的執行具有可再現性

    當線程並發執行時,由於資源共享和線程協作,使得線程之間會存在以下兩種制約關系。

      1):間接相互制約。一個系統中的多個線程必然要共享某種系統資源,如共享CPU,共享I/O設備,所謂間接制約即源於這種資源共享,打印機就是最好的例子,線程A在使用打印機時,其它線程都要等待。

      2):直接相互制約。這種制約主要時因為線程之間的合作,如有線程A將計算結果提供給線程B作進一步處理,那么線程B在線程A將數據送達之前都將會處於阻塞狀態

      間接相互制約可以稱為互斥,直接相互制約可以稱為同步,對於互斥可以這樣理解,線程A和線程B互斥訪問某個資源則它們之間就會產生順序問題——要么線程A等待線程B操作完畢,要么線程B等待線程操作完畢,這其實就是線程的同步了。因此同步包括互斥、互斥其實是一種特殊的同步

      例子:

        

  4、線程局部變量ThreadLocal

    A:ThreadLocal的作用和目的:用於實現線程內的數據共享,即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。

    B:每個線程調用全局ThreadLocal對象的set方法,在set方法中,首先根據當前線程獲取當前線程的ThreadLocalMap對象,然后往這個map中插入一條記錄,key其實是ThreadLocal對象,value是各自的set方法傳進去的值。也就是每個線程其實都有一份自己獨享的ThreadLocalMap對象,該對象的key是ThreadLocal對象,值是用戶設置的具體值。在線程結束時可以調用ThreadLocal.remove()方法,這樣會更快釋放內存,不調用也可以,因為線程結束后也可以自動釋放相關的ThreadLocal變量。

    C:ThreadLocal的應用場景:

      ➢ 訂單處理包含一系列操作:減少庫存量、增加一條流水台賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個線程中進行處理,如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的數據庫連接對象,而這些操作的代碼分別位於不同的模塊類中。
      ➢ 銀行轉賬包含一系列操作: 把轉出帳戶的余額減少,把轉入帳戶的余額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的數據庫連接對象,轉入和轉出操作的代碼分別是兩個不同的帳戶對象的方法。
      ➢ 例如 Strut2 的 ActionContext,同一段代碼被不同的線程調用運行時,該代碼操作的數據是每個線程各自的狀態和數據,對於不同的線程來說,getContext 方法拿到的對象都不相同,對同一個線程來說,不管調getContext 方法多少次和在哪個模塊中 getContext 方法,拿到的都是同一個。

  

    2、在Util類中創建ThreadLocal    

    3、在Runnable中創建ThreadLocal

       1):在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離的對象xxx。

       2):在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null的時候,應該new一個隔離訪問類型的對象,並強制轉換為要應用的類型。

       3):在ThreadDemo類的run()方法中,通過調用getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。

import java.util.Random;

public class ThreadLocalTest implements Runnable{
    ThreadLocal<Student> studentThreadLocal = new ThreadLocal<>();
    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName+"is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName+"is set age:"+age);
        //通過這個方法,為每個線程都獨立的new一個student對象,每個線程的student對象都可以設置不同的值
        Student student = getStudent();
        student.setAge(age);
        System.out.println(currentThreadName+"is first get age:"+student.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(currentThreadName+"is second get age:"+student.getAge());
    }

    private Student getStudent() {
        Student student = studentThreadLocal.get();
        if(null==student){
            student = new Student();
            studentThreadLocal.set(student);
        }
        return student;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A ");
        Thread t2 = new Thread(t,"Thread B ");
        t1.start();
        t2.start();
    }
}

class Student{
    private String name;
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Student() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

運行結果如下:

Thread B is running...
Thread A is running...
Thread B is set age:70
Thread A is set age:75
Thread A is first get age:75
Thread B is first get age:70
Thread A is second get age:75
Thread B is second get age:70

  5、多線程共享數據

      在Java傳統機制中的共享數據方式,大致可以簡單分為兩種情況:

      1):多個線程行為一致,共同操作一個數據源。也就是每個線程執行的代碼相同,可以使用同一個Runnable對象,這個Runnable對象中有那個共享數據,例如,賣票系統。

      2):多個線程行為不一致,共同操作一個數據源。也就是每個線程執行的代碼不同,這是需要用不用的Runnable對象。例如,銀行存取款。

      下面我們通過兩個示例代碼來分別說明這兩種方式:

    1、多個線程行為一致共同操作一個數據

      如果每個線程執行的代碼相同,可以使用同一個Runnable對象,這個Runnable對象中有那個共享數據,例如,買票系統。

    

/**
 * 共享數據類
 */
class ShareData1 {
    private int num = 10;

    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName() + ": invoke inc method num = " + num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 多線程類
 */

public class RunnableCusToInc implements Runnable {
    private ShareData1 shareData;

    public RunnableCusToInc(ShareData1 shareData) {
        this.shareData=shareData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            shareData.inc();
        }
    }

    /**
     * 測試方法
     */
    public static void main(String[] args) {
        ShareData1 shareData = new ShareData1();
        for(int i=0;i<4;i++){
            new Thread(new RunnableCusToInc(shareData),"Thread"+i).start();
        }
    }
}

運行結果如下:

Thread0: invoke inc method num = 11
Thread0: invoke inc method num = 12
Thread0: invoke inc method num = 13
Thread0: invoke inc method num = 14
Thread0: invoke inc method num = 15
Thread3: invoke inc method num = 16
Thread3: invoke inc method num = 17
Thread3: invoke inc method num = 18
Thread3: invoke inc method num = 19
Thread3: invoke inc method num = 20
Thread2: invoke inc method num = 21
Thread2: invoke inc method num = 22
Thread2: invoke inc method num = 23
Thread2: invoke inc method num = 24
Thread2: invoke inc method num = 25
Thread1: invoke inc method num = 26
Thread1: invoke inc method num = 27
Thread1: invoke inc method num = 28
Thread1: invoke inc method num = 29
Thread1: invoke inc method num = 30

    2、多個線程行為不一致共同操作一個數據

      如果每個線程執行的代碼不同,這時候需要用不用的Runnable對象,有如下兩種方式來實現這些Runnable對象之間的數據共享。

      1)將共享數據封裝在另外一個對象中,然后將這個對象逐一傳遞個各個Runnable對象。每個線程對共享數據的操作方法也分配到那個對象身上去完成,這樣容易實現針對該數據進行的各個操作的互斥和通信。

  

/**
 * 共享數據類
 */
class ShareData1 {
    private int num = 10;

    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName() + ": invoke inc method num = " + num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void dec() {
        num--;
        System.out.println(Thread.currentThread().getName() + ": invoke dec method num = " + num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 測試方法
     */
    public static void main(String[] args) {
        ShareData1 shareData = new ShareData1();
        for (int i = 0; i < 4; i++) {
            if (i % 2 == 0) {
                new Thread(new RunnableCusToInc(shareData), "Thread" + i).start();
            } else {
                new Thread(new RunnableCusToDec(shareData), "Thread" + i).start();
            }
        }
    }
}

/**
 * 多線程類
 */
class RunnableCusToInc implements Runnable {
    //封裝共享數據
    private ShareData1 shareData;

    public RunnableCusToInc(ShareData1 shareData) {
        this.shareData = shareData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            shareData.inc();
        }
    }
}

class RunnableCusToDec implements Runnable {
    //封裝共享數據
    private ShareData1 shareData;

    public RunnableCusToDec(ShareData1 shareData) {
        this.shareData = shareData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            shareData.dec();
        }
    }
}

運行結果如下:

Thread0: invoke inc method num = 11
Thread0: invoke inc method num = 12
Thread0: invoke inc method num = 13
Thread0: invoke inc method num = 14
Thread0: invoke inc method num = 15
Thread3: invoke dec method num = 14
Thread3: invoke dec method num = 13
Thread3: invoke dec method num = 12
Thread3: invoke dec method num = 11
Thread3: invoke dec method num = 10
Thread2: invoke inc method num = 11
Thread2: invoke inc method num = 12
Thread2: invoke inc method num = 13
Thread2: invoke inc method num = 14
Thread2: invoke inc method num = 15
Thread1: invoke dec method num = 14
Thread1: invoke dec method num = 13
Thread1: invoke dec method num = 12
Thread1: invoke dec method num = 11
Thread1: invoke dec method num = 10  

      2):將這些Runnable對象作為某一個類中的內部類,共享數據作為這個外部類中的成員變量,每個線程對共享數據的操作方法也分配給外部類,以便實現對共享數據進行的各個操作的互斥和通信,作為內部類的各個Runnable對象調用外部類的這些方法。

      

public class InnerThreadRunnable {
    public static void main(String[] args) {
        final ShareData2 shareData = new ShareData2();
        for(int i=0;i<4;i++){
            if(i%2==0){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        shareData.inc();
                    }
                },"Thread"+i).start();
            }else{
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        shareData.dec();
                    }
                },"Thread"+i).start();
            }
        }
    }
}
class ShareData2{
    private int num = 10 ;
    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void dec() {
        num--;
        System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行結果如下:

Thread0: invoke inc method num =11
Thread3: invoke dec method num =10
Thread2: invoke inc method num =11
Thread1: invoke dec method num =10

     補充:上面兩種方式的組合:將共享數據封裝在另外一個對象中,每個線程對共享數據的操作方法也分配到那個對象身上去完成,對象作為這個外部類中的成員變量或方法中的局部變量,每個線程的 Runnable 對象作為外部類中的成員內部類或局部內部類。

     總之,要同步互斥的幾段代碼最好是分別放在幾個獨立的方法中,這些方法再放在同一個類中,這樣比較容易實現它們之間的同步互斥和通信。


免責聲明!

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



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