java安全執行緒總結

來源:互聯網
上載者:User

最近想將java基礎的一些東西都整理整理,寫下來,這是對知識的總結,也是一種樂趣。已經擬好了提綱,大概分為這幾個主題: java安全執行緒,java垃圾收集,java並發包詳細介紹,java profile和jvm效能調優 。慢慢寫吧。本人jameswxx原創文章,轉載請註明出處,我費了很多心血,多謝了。關於java安全執行緒,網上有很多資料,我只想從自己的角度總結對這方面的考慮,有時候寫東西是很痛苦的,知道一些東西,但想用文字說清楚,卻不是那麼容易。我認為要認識java安全執行緒,必須瞭解兩個主要的點:java的記憶體模型,java的線程同步機制。特別是記憶體模型,java的線程同步機制很大程度上都是基於記憶體模型而設定的。後面我還會寫java並發包的文章,詳細總結如何利用java並發包編寫高效安全的多線程並發程式。暫時寫得比較倉促,後面會慢慢補充完善。

淺談java記憶體模型
不同的平台,記憶體模型是不一樣的,但是jvm的記憶體模型規範是統一的。其實java的多線程並發問題最終都會反映在java的記憶體模型上,所謂安全執行緒無非是要控制多個線程對某個資源的有序訪問或修改。總結java的記憶體模型,要解決兩個主要的問題:可見度和有序性。我們都知道電腦有快取的存在,處理器並不是每次處理資料都是取記憶體的。JVM定義了自己的記憶體模型,屏蔽了底層平台記憶體管理細節,對於java開發人員,要清楚在jvm記憶體模型的基礎上,如果解決多線程的可見度和有序性。
那麼,何謂可見度? 多個線程之間是不能互相傳遞資料通訊的,它們之間的溝通只能通過共用變數來進行。Java記憶體模型(JMM)規定了jvm有主記憶體,主記憶體是多個線程共用的。當new一個對象的時候,也是被分配在主記憶體中,每個線程都有自己的工作記憶體,工作記憶體儲存了主存的某些對象的副本,當然線程的工作記憶體大小是有限制的。當線程操作某個對象時,執行順序如下:
(1) 從主存複製變數到當前工作記憶體 (read and load)
(2) 執行代碼,改變共用變數值 (use and assign)
(3) 用工作記憶體資料重新整理主存相關內容 (store and write)
JVM規範定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共用變數在多個線程的工作記憶體中都有副本時,如果一個線程修改了這個共用變數,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見度問題。
那麼,什麼是有序性呢 ?線程在引用變數時不能直接從主記憶體中引用,如果線程工作記憶體中沒有該變數,則會從主記憶體中拷貝一個副本到工作記憶體中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該欄位時,有可能重新從主存中擷取變數副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現系統決定。
線程不能直接為主存中中欄位賦值,它會將值指定給工作記憶體中的變數副本(assign),完成後這個變數副本會同步到主儲存區(store-write),至於何時同步過去,根據JVM實現系統決定.有該欄位,則會從主記憶體中將該欄位賦值到工作記憶體中,這個過程為read-load,完成後線程會引用該變數副本,當同一線程多次重複對欄位賦值時,比如:
Java代碼 收藏代碼
for(int i=0;i<10;i++)
a++;

線程有可能只對工作記憶體中的副本進行賦值,只到最後一次賦值後才同步到主儲存區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共用變數x,線程a執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x加1
3 將x加1後的值寫回主 存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x減1
3 將x減1後的值寫回主存
那麼顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作記憶體,工作記憶體中x值為10
2:線程b從主存讀取x副本到工作記憶體,工作記憶體中x值為10
3:線程a將工作記憶體中x加1,工作記憶體中x值為11
4:線程a將x提交主存中,主存中x為11
5:線程b將工作記憶體中x值減1,工作記憶體中x值為9
6:線程b將x提交到中主存中,主存中x為9
同樣,x有可能為11,如果x是一個銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執行的,並且每個線程執行的加1或減1是一個原子操作。看看下面代碼:
Java代碼 收藏代碼
public class Account {

private int balance;

public Account(int balance) {
this.balance = balance;
}

public int getBalance() {
return balance;
}

public void add(int num) {
balance = balance + num;
}

public void withdraw(int num) {
balance = balance - num;
}

public static void main(String[] args) throws InterruptedException {
Account account = new Account(1000);
Thread a = new Thread(new AddThread(account, 20), "add");
Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
a.start();
b.start();
a.join();
b.join();
System.out.println(account.getBalance());
}

static class AddThread implements Runnable {
Account account;
int amount;

public AddThread(Account account, int amount) {
this.account = account;
this.amount = amount;
}

public void run() {
for (int i = 0; i < 200000; i++) {
account.add(amount);
}
}
}

static class WithdrawThread implements Runnable {
Account account;
int amount;

public WithdrawThread(Account account, int amount) {
this.account = account;
this.amount = amount;
}

public void run() {
for (int i = 0; i < 100000; i++) {
account.withdraw(amount);
}
}
}
}

第一次執行結果為10200,第二次執行結果為1060,每次執行的結果都是不確定的,因為線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對於同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執行有序性和記憶體可見度,而volatile關鍵字之解決多線程的記憶體可見度問題。後面將會詳細介紹。

synchronized關鍵字
上面說了,java用synchronized關鍵字做為多線程並發環境的執行有序性的保證手段之一。當一段代碼會修改共用變數,這一段代碼成為互斥區或臨界區,為了保證共用變數的正確性,synchronized標示了臨界區。典型的用法如下:
Java代碼 收藏代碼
synchronized(鎖){
臨界區代碼
}

為了保證銀行賬戶的安全,可以操作賬戶的方法如下:
Java代碼 收藏代碼
public synchronized void add(int num) {
balance = balance + num;
}
public synchronized void withdraw(int num) {
balance = balance - num;
}

剛才不是說了synchronized的用法是這樣的嗎:
Java代碼 收藏代碼
synchronized(鎖){
臨界區代碼
}

那麼對於public synchronized void add(int num)這種情況,意味著什麼呢?其實這種情況,鎖就是這個方法所在的對象。同理,如果方法是public static synchronized void add(int num),那麼鎖就是這個方法所在的class。
理論上,每個對象都可以做為鎖,但一個對象做為鎖時,應該被多個線程共用,這樣才顯得有意義,在並發環境下,一個沒有共用的對象作為鎖是沒有意義的。假如有這樣的代碼:
Java代碼 收藏代碼
public class ThreadTest{
public void test(){
Object lock=new Object();
synchronized (lock){
//do something
}
}
}

lock變數作為一個鎖存在根本沒有意義,因為它根本不是共用對象,每個線程進來都會執行Object lock=new Object();每個線程都有自己的lock,根本不存在鎖競爭。
每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒佇列儲存體了將要獲得鎖的線程,阻塞佇列儲存體了被阻塞的線程,當一個被線程被喚醒(notify)後,才會進入到就緒隊列,等待cpu的調度。當一開始線程a第一次執行account.add方法時,jvm會檢查鎖對象account的就緒隊列是否已經有線程在等待,如果有則表明account的鎖已經被佔用了,由於是第一次運行,account的就緒隊列為空白,所以線程a獲得了鎖,執行account.add方法。如果恰好在這個時候,線程b要執行account.withdraw方法,因為線程a已經獲得了鎖還沒有釋放,所以線程b要進入account的就緒隊列,等到得到鎖後才可以執行。
一個線程執行臨界區代碼過程如下:
1 獲得同步鎖
2 清空工作記憶體
3 從主存拷貝變數副本到工作記憶體
4 對這些變數計算
5 將變數從工作記憶體寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的並發有序性,又保證了多線程的記憶體可見度。

生產者/消費者模式
生產者/消費者模式其實是一種很經典的線程同步模型,很多時候,並不是光保證多個線程對某共用資源操作的互斥性就夠了,往往多個線程之間都是有協作的。
假設有這樣一種情況,有一個桌子,桌子上面有一個盤子,盤子裡只能放一顆雞蛋,A專門往盤子裡放雞蛋,如果盤子裡有雞蛋,則一直等到盤子裡沒雞蛋,B專門從盤子裡拿雞蛋,如果盤子裡沒雞蛋,則等待直到盤子裡有雞蛋。其實盤子就是一個互斥區,每次往盤子放雞蛋應該都是互斥的,A的等待其實就是主動放棄鎖,B等待時還要提醒A放雞蛋。
如何讓線程主動釋放鎖
很簡單,調用鎖的wait()方法就好。wait方法是從Object來的,所以任意對象都有這個方法。看這個程式碼片段:
Java代碼 收藏代碼
Object lock=new Object();//聲明了一個對象作為鎖
synchronized (lock) {
balance = balance - num;
//這裡放棄了同步鎖,好不容易得到,又放棄了
lock.wait();
}

如果一個線程獲得了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。如果調用lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。
聲明一個盤子,只能放一個雞蛋

Java代碼 收藏代碼
import java.util.ArrayList;
import java.util.List;

public class Plate {

List

eggs = new ArrayList

();

public synchronized Object getEgg() {
if (eggs.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
}
}

Object egg = eggs.get(0);
eggs.clear();// 清空盤子
notify();// 喚醒阻塞隊列的某線程到就緒隊列
System.out.println("拿到雞蛋");
return egg;
}

public synchronized void putEgg(Object egg) {
if (eggs.size() > 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
eggs.add(egg);// 往盤子裡放雞蛋
notify();// 喚醒阻塞隊列的某線程到就緒隊列
System.out.println("放入雞蛋");
}

static class AddThread extends Thread{
private Plate plate;
private Object egg=new Object();
public AddThread(Plate plate){
this.plate=plate;
}

public void run(){
for(int i=0;i<5;i++){
plate.putEgg(egg);
}
}
}

static class GetThread extends Thread{
private Plate plate;
public GetThread(Plate plate){
this.plate=plate;
}

public void run(){
for(int i=0;i<5;i++){
plate.getEgg();
}
}
}

public static void main(String args[]){
try {
Plate plate=new Plate();
Thread add=new Thread(new AddThread(plate));
Thread get=new Thread(new GetThread(plate));
add.start();
get.start();
add.join();
get.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("測試結束");
}
}
執行結果:
Html代碼 收藏代碼
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
測試結束

聲明一個Plate對象為plate,被線程A和線程B共用,A專門放雞蛋,B專門拿雞蛋。假設
1 開始,A調用plate.putEgg方法,此時eggs.size()為0,因此順利將雞蛋放到盤子,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列還沒有線程。
2 又有一個A線程對象調用plate.putEgg方法,此時eggs.size()不為0,調用wait()方法,自己進入了鎖對象的阻塞隊列。
3 此時,來了一個B線程對象,調用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個雞蛋,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒後,它進入到就緒隊列,就緒隊列也就它一個,因此馬上得到鎖,開始往盤子裡放雞蛋,此時盤子是空的,因此放雞蛋成功。
4 假設接著來了線程A,就重複2;假設來料線程B,就重複3。
整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

volatile關鍵字
volatile是java提供的一種同步手段,只不過它是輕量級的同步,為什麼這麼說,因為volatile只能保證多線程的記憶體可見度,不能保證多線程的執行有序性。而最徹底的同步要保證有序性和可見度,例如synchronized。任何被volatile修飾的變數,都不拷貝副本到工作記憶體,任何修改都及時寫在主存。因此對於Valatile修飾的變數的修改,所有線程馬上就能看到,但是volatile不能保證對變數的修改是有序的。什麼意思呢?假如有這樣的代碼:
Java代碼 收藏代碼
public class VolatileTest{
public volatile int a;
public void add(int count){
a=a+count;
}
}

當一個VolatileTest對象被多個線程共用,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作記憶體和主存的同步。所以,volatile的使用情境是有限的,在有限的一些情形下可以使用 volatile 變數替代鎖。要使 volatile 變數提供理想的安全執行緒,必須同時滿足下面兩個條件:
1)對變數的寫操作不依賴於當前值。
2)該變數沒有包含在具有其他變數的不變式中
volatile只保證了可見度,所以Volatile適合直接賦值的情境,如
Java代碼 收藏代碼
public class VolatileTest{
public volatile int a;
public void setA(int a){
this.a=a;
}
}

在沒有volatile聲明時,多線程環境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作記憶體和同步a到主存的步驟,相當於是一個原子操作。所以簡單來說,volatile適合這種情境:一個變數被多個線程共用,線程直接給這個變數賦值。這是一種很簡單的同步情境,這時候使用volatile的開銷將會非常小。


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.