文章出處

一、進程與線程

1、進程

     進程是操作系統結構的基礎;是一次程序的執行;是一個程序及其數據在處理機上順序執行時所發生的活動。操作系統中,幾乎所有運行中的任務對應一條進程(Process)。一個程序進入內存運行,即變成一個進程。進程是處于運行過程中的程序,并且具有一定獨立功能。描述進程的有一句話非常經典的話——進程是系統進行資源分配和調度的一個獨立單位。

     進程是系統中獨立存在的實體,擁有自己獨立的資源,擁有自己私有的地址空間進程的實質,就是程序在多道程序系統中的一次執行過程,它是動態產生,動態消亡的,具有自己的生命周期和各種不同的狀態。進程具有并發性,它可以同其他進程一起并發執行,按各自獨立的、不可預知的速度向前推進。 

(注意,并發性(concurrency)和并行性(parallel)是不同的。并行指的是同一時刻,多個指令在多臺處理器上同時運行。并發指的是同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,看起來就好像多個指令同時執行一樣。

    進程由程序數據進程控制塊三部分組成。

2、線程

     線程,有時被稱為輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以并發執行。由于線程之間的相互制約,致使線程在運行中呈現出間斷性。每一個程序都至少有一個線程,若程序只有一個線程,那就是程序本身。

    線程是程序中一個單一的順序控制流程。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。

    在Java Web中要注意,線程是JVM級別的,在不停止的情況下,跟JVM共同消亡,就是說如果一個Web服務啟動了多個Web應用,某個Web應用啟動了某個線程,如果關閉這個Web應用,線程并不會關閉,因為JVM還在運行,所以別忘了設置Web應用關閉時停止線程。

二、線程的生命周期及五種基本狀態

關于Java中線程的生命周期,首先看一下下面這張較為經典的圖:

上圖中基本上囊括了Java中多線程各重要知識點。掌握了上圖中的各知識點,Java中的多線程也就基本上掌握了。

Java線程具有五種基本狀態

新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread()。

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,并不是說執行了t.start()此線程立即就會執行。

運行狀態(Running):當CPU開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中。

阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:

1.等待阻塞 -- 運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

三、創建多線程的方式

Java中線程的創建有如下三種基本形式。

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

package com.demo.test;

public class MyThread extends Thread {
    
    private String name;
    
    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "運行  :" + i);
        }
    }
}
package com.demo.test;

public class ThreadTest {

     public static void main(String[] args) {
         Thread myThread1 = new MyThread("A");     // 創建一個新的線程  myThread1  此線程進入新建狀態
         Thread myThread2 = new MyThread("B");     // 創建一個新的線程 myThread2 此線程進入新建狀態
         myThread1.start();  // 調用start()方法使得線程進入就緒狀態
         myThread2.start();  // 調用start()方法使得線程進入就緒狀態
    }
}

運行結果:

A運行  :0
B運行  :0
B運行  :1
B運行  :2
B運行  :3
B運行  :4
B運行  :5
B運行  :6
B運行  :7
B運行  :8
A運行  :1
A運行  :2
A運行  :3
B運行  :9
A運行  :4
A運行  :5
A運行  :6
A運行  :7
A運行  :8
A運行  :9

     如上所示,繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之為線程執行體。當創建此線程類對象時一個新的線程得以創建,并進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程并不一定會馬上得以執行,這取決于CPU調度時機。

2、實現java.lang.Runnable接口 

具體做法:實現Runnable接口,并重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,并以此實例作為Thread類的target來創建Thread對象,該Thread對象才是真正的線程對象。

package com.demo.test;

public class MyRunnable implements Runnable{

    private String name;
    
    public MyRunnable(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "運行  :" + i);
        }
    }
}
package com.demo.test;

public class ThreadTest {

     public static void main(String[] args) {

         Runnable myRunnable = new MyRunnable("A"); // 創建一個Runnable實現類的對象
         Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target創建新的線程
         Runnable myRunnable1 = new MyRunnable("B");
         Thread thread2 = new Thread(myRunnable1);
         thread1.start(); // 調用start()方法使得線程進入就緒狀態
         thread2.start();
    }
}

運行結果:

A運行  :0
B運行  :0
B運行  :1
B運行  :2
A運行  :1
A運行  :2
B運行  :3
A運行  :3
A運行  :4
B運行  :4
A運行  :5
A運行  :6
A運行  :7
B運行  :5
A運行  :8
A運行  :9
B運行  :6
B運行  :7
B運行  :8
B運行  :9

3、使用Callable和Future接口創建線程。

具體是創建Callable接口的實現類,并實現call()方法。并使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作為Thread對象的target來創建線程。

package com.demo.test;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer>{
    
    // 與run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() throws Exception{
        System.out.println("子線程在進行計算");
        Thread.sleep(3000);
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            //System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}
package com.demo.test;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {

     public static void main(String[] args) {
         
         Callable<Integer> myCallable = new MyCallable();    // 創建MyCallable對象
         FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象

         Thread thread = new Thread(ft);  //FutureTask對象作為Thread對象的target創建新的線程
         thread.start();  //線程進入到就緒狀態

         try {
             Thread.sleep(1000);
         } catch (InterruptedException e1) {
             e1.printStackTrace();
         }
         
         System.out.println("主線程在執行任務");
        
         try {
             int sum = ft.get(); //取得新創建的線程中的call()方法返回的結果
             System.out.println("task運行結果,sum = " + sum);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
         
         System.out.println("所有任務執行完畢");
        
    }
}

運行結果:

子線程在進行計算
主線程在執行任務
task運行結果,sum = 4950
所有任務執行完畢

     首先,我們發現,在實現Callable接口中,此時不再是run()方法了,而是call()方法,此call()方法作為線程執行體,同時還具有返回值!在創建新的線程時,是通過FutureTask來包裝MyCallable對象,同時作為了Thread對象的target。那么看下FutureTask類的定義:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口的實現:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

     于是,我們發現FutureTask類實際上是同時實現了Runnable和Future接口,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作為Thread對象的target,而Future特性,使得其可以取得新創建線程中的call()方法的返回值。

     執行下此程序,我們發現sum = 4950永遠都是最后輸出的。那么為什么sum =4950會永遠最后輸出呢?原因在于通過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。

    上述主要講解了三種常見的線程創建方式,對于線程的啟動而言,都是調用線程對象的start()方法,需要特別注意的是:不能對同一線程對象兩次調用start()方法。

四、線程調度

1、線程加入——join()

join —— 讓一個線程等待另一個線程完成才繼續執行。如A線程執行體中調用B線程的join()方法,則A線程被阻塞,直到B線程執行完為止,A才能得以繼續執行。

join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個線程運行結束,當前線程再由阻塞轉為就緒狀態。

join是Thread類的一個方法,啟動線程后直接調用,join() 的作用:讓“主線程”等待“子線程”結束之后才能繼續運行。 

為什么要用join() 方法?

     在很多情況下,主線程生成并起動了子線程,如果子線程里要進行大量的耗時的運算,主線程往往將于子線程之前結束,但是如果主線程處理完其他的事務后,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之后再結束,這個時候就要用到join()方法了。

不加join的情況:

package com.demo.test;

public class Thread1 extends Thread{
    
    private String name;
    public Thread1(String name) {
        super(name);
        this.name=name;
    }
    
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 線程運行開始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子線程"+name + "運行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 線程運行結束!");
    }
}
package com.demo.test;

public class Main {
    
    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
    }
}

運行結果:

main主線程運行開始!
A 線程運行開始!
B 線程運行開始!
main主線程運行結束!
子線程B運行 : 0
子線程A運行 : 0
子線程B運行 : 1
子線程A運行 : 1
子線程B運行 : 2
子線程A運行 : 2
子線程B運行 : 3
子線程A運行 : 3
子線程B運行 : 4
子線程A運行 : 4
B 線程運行結束!
A 線程運行結束!

加join 的情況:

package com.demo.test;

public class Main {
    
    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
    }
}

運行結果:

main主線程運行開始!
A 線程運行開始!
B 線程運行開始!
子線程A運行 : 0
子線程B運行 : 0
子線程A運行 : 1
子線程B運行 : 1
子線程A運行 : 2
子線程B運行 : 2
子線程A運行 : 3
子線程B運行 : 3
子線程A運行 : 4
子線程B運行 : 4
A 線程運行結束!
B 線程運行結束!
main主線程運行結束!

2、線程睡眠——sleep()

sleep()是Thread類的靜態方法。該方法聲明拋出了InterrupedException異常。所以使用時,要么捕捉,要么聲明拋出。有兩種重載方式:

static void sleep(long millis); //讓當前正在執行的線程暫停millis毫秒,并進入阻塞狀態,該方法受到系統計時器和線程調度器的精度和準度的影響。

static void sleep(long millis , int nanos);  //讓當前正在執行的線程暫停millis毫秒加nanos微秒,并進入阻塞狀態,該方法受到系統計時器和線程調度器的
精度和準度的影響。

sleep() 的作用是讓當前線程休眠,即當前線程會從“運行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,線程休眠的時間會大于/等于該休眠時間;在線程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”,從而等待cpu的調度執行。常用來暫停程序的運行。同時注意,sleep()方法不會釋放鎖

3、線程讓步——yield()

     yield()是Thread類的靜態方法。它能讓當前線程暫停,但不會阻塞該線程,而是由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,并不能保證在當前線程調用yield()之后,其它具有相同優先級的線程就一定能獲得執行權,也有可能是當前線程又進入到“運行狀態”繼續運行!值得注意的是,yield()方法不會釋放鎖

package com.demo.test;

public class ThreadYield extends Thread{
    
    public ThreadYield(String name) {
        super(name);
    }
 
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 當i為30時,該線程就會把CPU時間讓掉,讓其他或者自己的線程執行(也就是誰先搶到誰執行)
            if (i ==30) {
                this.yield();
            }
        }
    }

}
package com.demo.test;

public class Main {
    
    public static void main(String[] args) {

        ThreadYield yt1 = new ThreadYield("A");
        ThreadYield yt2 = new ThreadYield("B");
        yt1.start();
        yt2.start();

    }
}

運行情況:

第一種情況:A線程當執行到30時會將CPU時間讓掉,這時B線程搶到CPU時間并執行。

第二種情況:A線程當執行到30時會將CPU時間讓掉,這時A線程搶到CPU時間并執行。

第三種情況:B線程當執行到30時會將CPU時間讓掉,這時A線程搶到CPU時間并執行。

第四種情況:B線程當執行到30時會將CPU時間讓掉,這時B線程搶到CPU時間并執行。

4、線程中斷——interrupt()

     我們經常通過判斷線程的中斷標記來控制線程。   

  interrupt()是Thread類的一個實例方法,用于中斷本線程。這個方法被調用時,會立即將線程的中斷標志設置為“true”。所以當中斷處于“阻塞狀態”的線程時,由于處于阻塞狀態,中斷標記會被設置為“false”,拋出一個 InterruptedException。所以我們在線程的循環外捕獲這個異常,就可以退出線程了。

  interrupt()并不會中斷處于“運行狀態”的線程,它會把線程的“中斷標記”設置為true,所以我們可以不斷通過isInterrupted()來檢測中斷標記,從而在調用了interrupt()后終止線程,這也是通常我們對interrupt()的用法。

  interrupted()是Thread類的一個靜態方法,它返回一個布爾類型指明當前線程是否已經被中斷,isInterrupted()是Thread類的實例方法,返回一個布爾類型來判斷線程是否已經被中斷。它們都能夠用于檢測對象的“中斷標記”。區別是,interrupted()除了返回中斷標記之外,它還會清除中斷標記(即將中斷標記設為false);而isInterrupted()僅僅返回中斷標記。

綜合線程處于“阻塞狀態”和“運行狀態”的終止方式,比較通用的終止線程的形式如下:

@Override
public void run() {
  try {
      // 1. isInterrupted()保證,只要中斷標記為true就終止線程。
      while (!isInterrupted()) {
          // 執行任務...
      }
  } catch (InterruptedException ie) {
      // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
  }
}

interrupted() 和 isInterrupted() 的比較

首先看一下API中該方法的實現: 

public static boolean interrupted() { 
    return currentThread().isInterrupted(true); 
} 

該方法就是直接調用當前線程的isInterrupted(true)的方法。

然后再來看一下API中 isInterrupted的實現:

public boolean isInterrupted() { 
    return isInterrupted(false); 
} 

該方法卻直接調用當前線程的isInterrupted(false)的方法。

因此這兩個方法有兩個主要區別:

(1)interrupted 是作用于當前線程,isInterrupted 是作用于調用該方法的線程對象所對應的線程。(線程對象對應的線程不一定是當前運行的線程。例如我們可以在A線程中去調用B線程對象的isInterrupted方法。)

(2)這兩個方法最終都會調用同一個方法-----isInterrupted( Boolean 參數),只不過參數固定為一個是true,一個是false。注意:isInterrupted( Boolean 參數)是isInterrupted()的重載方法。

由于第二個區別主要體現在調用的方法的參數上,讓我們來看一看這個參數是什么含義。先來看一看被調用的方法 isInterrupted(boolean arg)(Thread類中重載的方法)的定義:

private native boolean isInterrupted( boolean ClearInterrupted);
     原來這是一個本地方法,看不到源碼。不過沒關系,通過參數名ClearInterrupted我們就能知道,這個參數代表是否要清除狀態位。如果這個參數為true,說明返回線程的狀態位后,要清掉原來的狀態位(恢復成原來情況)。這個參數為false,就是直接返回線程的狀態位。這兩個方法很好區分,只有當前線程才能清除自己的中斷位(對應interrupted()方法)。 

五、線程安全與線程同步

1、一個典型的Java線程安全例子

package com.demo.test;

public class Account {

    private String accountNo;
    private double balance;

    public Account() {

    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

}
package com.demo.test;

public class DrawMoneyRunnable implements Runnable{
    
    private Account account;
    private double drawAmount;

    public DrawMoneyRunnable(Account account, double drawAmount) {
        super();
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run() {
        if (account.getBalance() >= drawAmount) {  //1
            System.out.println("取錢成功, 取出錢數為:" + drawAmount);
            double balance = account.getBalance() - drawAmount;
            account.setBalance(balance);
            System.out.println("余額為:" + balance);
        }
    }

}
package com.demo.test;

public class ThreadTest {

    public static void main(String[] args) {
        Account account = new Account("123456", 1000);
        DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
        Thread myThread1 = new Thread(drawMoneyRunnable);
        Thread myThread2 = new Thread(drawMoneyRunnable);
        myThread1.start();
        myThread2.start();
    }

}

    上面例子很容易理解,有一張銀行卡,里面有1000的余額,程序模擬兩個人同時在取款機進行取錢操作的場景。多次運行此程序,可能具有多個不同組合的輸出結果。其中一種可能的輸出為:

取錢成功, 取出錢數為:700.0
余額為:300.0
取錢成功, 取出錢數為:700.0
余額為:-400.0

     也就是說,對于一張只有1000余額的銀行卡,你們一共可以取出1400,這顯然是有問題的。

     經過分析,問題在于Java多線程環境下的執行的不確定性。CPU可能隨機的在多個處于就緒狀態中的線程中進行切換,因此,很有可能出現如下情況:當thread1執行到//1處代碼時,判斷條件為true,此時CPU切換到thread2,執行//1處代碼,發現依然為真,然后執行完thread2,接著切換到thread1,接著執行完畢。此時,就會出現上述結果。

     因此,講到線程安全問題,其實是指多線程環境下對共享資源的訪問可能會引起此共享資源的不一致性。因此,為避免線程安全問題,應該避免多線程環境下對此共享資源的并發訪問。

2、多線程的同步

為何要使用同步? 

     java允許多線程并發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生沖突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。

(1)同步方法

     對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱為同步方法。可以簡單理解成對此方法進行了加鎖,其鎖對象為當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要獲得此同步鎖(且同時最多只有一個線程能夠獲得),只有當線程執行完此同步方法后,才會釋放鎖對象,其他的線程才有可能獲取此同步鎖,以此類推...。

     在上例中,共享資源為account對象,當使用同步方法時,可以解決線程安全問題。只需在run()方法前加上synchronized關鍵字即可。

public synchronized void run() {
       
    // ....
 
}

(2)同步代碼塊

     正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不確定性即可。使用同步方法時,使得整個方法體都成為了同步執行狀態,會使得可能出現同步范圍過大的情況,于是,針對需要同步的代碼可以直接另一種同步方式——同步代碼塊來解決。

同步代碼塊的格式為:

synchronized (obj) {
            
    //...

}

      其中,obj為鎖對象,因此,選擇哪一個對象作為鎖是至關重要的。一般情況下,都是選擇此共享資源對象作為鎖對象。

      如上例中,最好選用account對象作為鎖對象。(當然,選用this也是可以的,那是因為創建線程使用了runnable方式,如果是直接繼承Thread方式創建的線程,使用this對象作為同步鎖其實沒有起到任何作用,因為是不同的對象了。因此,選擇同步鎖時需要格外小心...)

關于synchronized關鍵字的說明:

 ① 原理

    在java中,每一個對象有且僅有一個同步鎖。這也意味著,同步鎖是依賴于對象而存在。當前線程調用某對象的synchronized方法時,就獲取了該對象的同步鎖。例如,synchronized(obj),當前線程就獲取了“obj這個對象”的同步鎖。

    不同線程對同步鎖的訪問是互斥的。也就是說,某時間點,對象的同步鎖只能被一個線程獲取到!通過同步鎖,我們就能在多線程中,實現對“對象/方法”的互斥訪問。 例如,現在有個線程A和線程B,它們都會訪問“對象obj的同步鎖”。假設,在某一時刻,線程A獲取到“obj的同步鎖”并在執行一些操作;而此時,線程B也企圖獲取“obj的同步鎖” —— 線程B會獲取失敗,它必須等待,直到線程A釋放了“該對象的同步鎖”之后線程B才能獲取到“obj的同步鎖”從而才可以運行。

② 基本規則

第一條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的該“synchronized方法”或者“synchronized代碼塊”的訪問將被阻塞。

第二條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程仍然可以訪問“該對象”的非同步代碼塊

第三條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的其他的“synchronized方法”或者“synchronized代碼塊”的訪問將被阻塞。

③ 實例鎖和全局鎖

實例鎖 -- 鎖在某一個實例對象上。如果該類是單例,那么該鎖也具有全局鎖的概念。實例鎖對應的就是synchronized關鍵字。

全局鎖 -- 該鎖針對的是類,無論實例多少個對象,那么線程都共享該鎖。全局鎖對應的就是static synchronized(或者是鎖在該類的class或者classloader對象上)。

就是說,一個非靜態方法上的synchronized關鍵字,代表該方法依賴其所屬對象。一個靜態方法上synchronized關鍵字,代表該方法依賴這個類本身。

六、線程通信 wait()/notify()/notifyAll()

wait():導致當前線程等待并使其進入到等待阻塞狀態。直到其他線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。

notify():喚醒在此同步鎖對象上等待的單個線程,如果有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操作,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。

notifyAll():喚醒在此同步鎖對象上等待的所有線程,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。

例子:

package com.demo.test;

public class Account {

    private String accountNo;
    private double balance;
    // 標識賬戶中是否已有存款
    private boolean flag = false;

    public Account() {

    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
    
    /**
     * 存錢
     * 
     * @param depositeAmount
     */
    public synchronized void deposite(double depositeAmount, int i) {

        if (flag) {
            // 賬戶中已有人存錢進去,此時當前線程需要等待阻塞
            try {
                System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " -- i=" + i);
                wait();
                // 1
                System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " -- i=" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            // 開始存錢
            System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
            setBalance(balance + depositeAmount);
            flag = true;

            // 喚醒其他線程
            notifyAll();

            // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-- 存錢 -- 執行完畢" + " -- i=" + i);
        }
    }

    /**
     * 取錢
     * 
     * @param drawAmount
     */
    public synchronized void draw(double drawAmount, int i) {
        if (!flag) {
            // 賬戶中還沒人存錢進去,此時當前線程需要等待阻塞
            try {
                System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " -- i=" + i);
                wait();
                System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " -- i=" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            // 開始取錢
            System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount + " -- i=" + i);
            setBalance(getBalance() - drawAmount);

            flag = false;

            // 喚醒其他線程
            notifyAll();

            System.out.println(Thread.currentThread().getName() + "-- 取錢 -- 執行完畢" + " -- i=" + i); // 3
        }
    }

}
package com.demo.test;

public class DrawMoneyThread extends Thread{
    
    private Account account;
    private double amount;

    public DrawMoneyThread(String threadName, Account account, double amount) {
        super(threadName);
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            account.draw(amount, i);
        }
    }

}
package com.demo.test;

public class DepositeMoneyThread extends Thread{
    
    private Account account;
    private double amount;

    public DepositeMoneyThread(String threadName, Account account, double amount) {
        super(threadName);
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            account.deposite(amount, i);
        }
    }

}
package com.demo.test;

public class ThreadTest{

    public static void main(String[] args) {
        Account account = new Account("123456", 0);

        Thread drawMoneyThread = new DrawMoneyThread("取錢線程", account, 700);
        Thread depositeMoneyThread = new DepositeMoneyThread("存錢線程", account, 700);

        drawMoneyThread.start();
        depositeMoneyThread.start();
    }

}

運行結果:

取錢線程 開始要執行wait操作 -- i=0
存錢線程 存款:700.0 -- i=0
存錢線程-- 存錢 -- 執行完畢 -- i=0
存錢線程 開始要執行wait操作 -- i=1
取錢線程 執行了wait操作 -- i=0
取錢線程 取錢:700.0 -- i=1
取錢線程-- 取錢 -- 執行完畢 -- i=1
取錢線程 開始要執行wait操作 -- i=2
存錢線程 執行了wait操作 -- i=1
存錢線程 存款:700.0 -- i=2
存錢線程-- 存錢 -- 執行完畢 -- i=2
存錢線程 開始要執行wait操作 -- i=3
取錢線程 執行了wait操作 -- i=2
取錢線程 取錢:700.0 -- i=3
取錢線程-- 取錢 -- 執行完畢 -- i=3
取錢線程 開始要執行wait操作 -- i=4
存錢線程 執行了wait操作 -- i=3
存錢線程 存款:700.0 -- i=4
存錢線程-- 存錢 -- 執行完畢 -- i=4
取錢線程 執行了wait操作 -- i=4

由此,我們需要注意如下幾點:

1.wait()方法執行后,當前線程立即進入到等待阻塞狀態,其后面的代碼不會執行;

2.notify()/notifyAll()方法執行后,將喚醒此同步鎖對象上的(任意一個-notify()/所有-notifyAll())線程對象,但是,此時還并沒有釋放同步鎖對象,也就是說,如果notify()/notifyAll()后面還有代碼,還會繼續執行,直到當前線程執行完畢才會釋放同步鎖對象;

3.notify()/notifyAll()執行后,如果下面有sleep()方法,則會使當前線程進入到阻塞狀態,但是同步對象鎖沒有釋放,依然自己保留,那么一定時候后還是會繼續執行此線程,接下來同2;

4.wait()/notify()/nitifyAll()完成線程間的通信或協作都是基于相同對象鎖的,因此,如果是不同的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關系;

5.當wait線程喚醒后并執行時,是接著上次執行到的wait()方法代碼后面繼續往下執行的。

當然,上面的例子相對來說比較簡單,只是為了簡單示例wait()/notify()/noitifyAll()方法的用法,但其本質上說,已經是一個簡單的生產者-消費者模式了。

七、線程優先級和守護線程

1、線程優先級

      java中的線程優先級的范圍是1~10,默認的優先級是5。每個線程默認的優先級都與創建它的父線程具有相同的優先級。默認情況下,mian線程具有普通優先級。“高優先級線程”會優先于“低優先級線程”執行。Thread提供了setPriority(int newPriority)和getPriority()方法來設置和返回線程優先級。

Thread類有3個靜態常量:

 MAX_PRIORITY = 10

 MIN_PRIORITY = 1

 NORM_PRIORITY = 5

2、守護線程

      java 中有兩種線程:用戶線程和守護線程。可以通過isDaemon()方法來區別它們:如果返回false,則說明該線程是“用戶線程”;否則就是“守護線程”。用戶線程一般用戶執行用戶級任務,而守護線程也就是“后臺線程”,一般用來執行后臺任務。需要注意的是:Java虛擬機在“用戶線程”都結束后會后退出。

看一個例子,主線程中建立一個守護線程,當主線程結束時,守護線程也跟著結束。

package com.demo.test;

public class DaemonThreadTest {

     public static void main(String[] args){
         
        Thread mainThread = new Thread(new Runnable(){
            @Override
            public void run(){
                Thread childThread = new Thread(new ClildThread());
                childThread.setDaemon(true);
                childThread.start();
                System.out.println("I'm main thread...");
            }
        });
        mainThread.start();
    }
}
package com.demo.test;

import java.util.concurrent.TimeUnit;

public class ClildThread implements Runnable{

    @Override
    public void run(){
        
        while(true){
            System.out.println("I'm child thread..");
            try{
                TimeUnit.MILLISECONDS.sleep(1000);
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

運行結果:

I'm main thread...
I'm child thread..

如果不指定childThread為守護線程,當主線程結束時,childThread還在繼續運行,如下:

package com.demo.test;

public class DaemonThreadTest {

     public static void main(String[] args){
         
        Thread mainThread = new Thread(new Runnable(){
            @Override
            public void run(){
                Thread childThread = new Thread(new ClildThread());
                childThread.setDaemon(false);
                childThread.start();
                System.out.println("I'm main thread...");
            }
        });
        mainThread.start();
    }
}
package com.demo.test;

import java.util.concurrent.TimeUnit;

public class ClildThread implements Runnable{

    @Override
    public void run(){
        
        while(true){
            System.out.println("I'm child thread..");
            try{
                TimeUnit.MILLISECONDS.sleep(1000);
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

運行結果:

I'm main thread...
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..(無限輸出)

可以看到,當主線程結束時,childThread是非守護線程,就會無限的執行。

將線程轉換為守護線程可以通過調用Thread對象的setDaemon(true)方法來實現。在使用守護線程時需要注意一下幾點:

(1) thread.setDaemon(true)必須在thread.start()之前設置,否則會拋出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。 

(2) 在Daemon線程中產生的新線程也是Daemon的。

(3) 守護線程應該永遠不去訪問固有資源,如文件、數據庫,因為它會在任何時候甚至在一個操作的中間發生中斷。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()