java併發之線程同步(synchronized和鎖機制)

 

正文html

多個執行線程共享一個資源的情景,是併發編程中最多見的情景之一。多個線程讀或者寫相同的數據等狀況時可能會致使數據不一致。爲了解決這些問題,引入了臨界區概念。臨界區是一個用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只容許一個線程執行。java

Java提供了同步機制。當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是否是已有其餘線程進入臨界區。若是沒有其餘線程進入臨界區,它就能夠進入臨界區;若是已有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。若是在等待進入臨界區的線程不止一個,JVM會 隨機選擇其中的一個,其他的將繼續等待。
概念比較好理解,具體在java程序中是如何體現的呢?臨界區對應的代碼是怎麼樣的?

使用synchronized實現同步方法

每個用synchronized關鍵字聲明的方法都是臨界區。在Java中, 同一個對象的臨界區,在同一時間只有一個容許被訪問。
注意:用synchronized關鍵字聲明的靜態方法,同時只能被一個執行線程訪問,可是其餘線程能夠訪問這個對象的非靜態方法。即:兩個線程能夠同時訪問一個對象的兩個不一樣的synchronized方法,其中一個是靜態方法,一個是非靜態方法。
知道了synchronized關鍵字的做用,再來看一下synchronized關鍵字的使用方式。
  • 在方法聲明中加入synchronized關鍵字
  • 1 public synchronized void addAmount(double amount) {
    2 }
  • 在代碼塊中使用synchronized關鍵字,obj通常可使用this關鍵字表示本類對象
  • 1 synchronized(obj){
    2 }
須要注意的是:前面已經提到,引入synchronized關鍵字是爲了聲明臨界區,解決在多線程環境下共享變量的數據更改安全問題。那麼,通常用到synchronized關鍵字的地方也就是 在對共享數據 訪問或者修改的地方。下面舉一個例子,例子場景是這樣:公司定時會給帳戶打款,銀行對帳戶進行扣款。那麼款項對於銀行和公司來講就是一個共享數據。那麼synchronized關鍵字就應該在修改帳戶的地方使用。
聲明一個Account類:
複製代碼
 1 public class Account {
 2     private double balance;
 3     public double getBalance() {
 4         return balance;
 5     }
 6     public void setBalance(double balance) {
 7         this.balance = balance;
 8     }
 9     public synchronized void addAmount(double amount) {
10         double tmp=balance;
11         try {
12             Thread.sleep(10);
13         } catch (InterruptedException e) {
14             e.printStackTrace();
15         }
16         tmp+=amount;
17         balance=tmp;
18     }
19     public synchronized void subtractAmount(double amount) {
20         double tmp=balance;
21         try {
22             Thread.sleep(10);
23         } catch (InterruptedException e) {
24             e.printStackTrace();
25         }
26         tmp-=amount;
27         balance=tmp;
28     }
29 }
複製代碼
Bank類扣款:
複製代碼
 1 public class Bank implements Runnable {
 2     private Account account;
 3     public Bank(Account account) {
 4         this.account=account;
 5     }
 6     public void run() {
 7         for (int i=0; i<100; i++){
 8             account.subtractAmount(1000);
 9         }
10     }
11 }
複製代碼
Company類打款:
複製代碼
 1 public class Company implements Runnable {
 2     private Account account;
 3     public Company(Account account) {
 4         this.account=account;
 5     }
 6 
 7     public void run() {
 8         for (int i=0; i<100; i++){
 9             account.addAmount(1000);
10         }
11     }
12 }
複製代碼
這裏須要注意的就是:在Bank和Company的構造函數裏面傳遞的參數是Account,就是一個共享數據。
Main函數:
複製代碼
 1 public class Main {
 2     public static void main(String[] args) {
 3         Account    account=new Account();
 4         account.setBalance(1000);
 5         Company    company=new Company(account);
 6         Thread companyThread=new Thread(company);
 7         Bank bank=new Bank(account);
 8         Thread bankThread=new Thread(bank);
 9 
10         companyThread.start();
11         bankThread.start();
12         try {
13             companyThread.join();
14             bankThread.join();
15             System.out.printf("Account : Final Balance: %f\n",account.getBalance());
16         } catch (InterruptedException e) {
17             e.printStackTrace();
18         }
19     }
20 }
複製代碼
這個例子比較簡單,可是能夠說明問題。
補充:
一、synchronized關鍵字會下降應用程序的性能,所以只能在併發場景中修改共享數據的方法上使用它。
二、臨界區的訪問應該儘量的短。方法的其他部分保持在synchronized代碼塊以外,以獲取更好的性能

使用非依賴屬性實現同步

非依賴屬性:例如在一個類中有兩個非依賴屬性,Object obj1,Object obj2;他們被多個線程共享,那麼同一時間只容許一個線程訪問其中的一個屬性變量,其餘的 某個線程訪問另外一個屬性變量。
舉例以下:兩個看電影的房間和兩個售票口,一個售票處賣出的一張票,只能用於其中的一個電影院。不能同時做用於兩個電影房間。
Cinema類:
複製代碼
 1 public class Cinema {
 2     private long vacanciesCinema1;
 3     private long vacanciesCinema2;
 4 
 5     private final Object controlCinema1, controlCinema2;
 6 
 7     public Cinema(){
 8         controlCinema1=new Object();
 9         controlCinema2=new Object();
10         vacanciesCinema1=20;
11         vacanciesCinema2=20;
12     }
13     
14     public boolean sellTickets1 (int number) {
15         synchronized (controlCinema1) {
16             if (number<vacanciesCinema1) {
17                 vacanciesCinema1-=number;
18                 return true;
19             } else {
20                 return false;
21             }
22         }
23     }
24     
25     public boolean sellTickets2 (int number){
26         synchronized (controlCinema2) {
27             if (number<vacanciesCinema2) {
28                 vacanciesCinema2-=number;
29                 return true;
30             } else {
31                 return false;
32             }
33         }
34     }
35     
36     public boolean returnTickets1 (int number) {
37         synchronized (controlCinema1) {
38             vacanciesCinema1+=number;
39             return true;
40         }
41     }
42     public boolean returnTickets2 (int number) {
43         synchronized (controlCinema2) {
44             vacanciesCinema2+=number;
45             return true;
46         }
47     }
48     public long getVacanciesCinema1() {
49         return vacanciesCinema1;
50     }
51     public long getVacanciesCinema2() {
52         return vacanciesCinema2;
53     }
54 }
複製代碼
這樣的話,vacanciescinema1和vacanciescinema2(剩餘票數)是獨立的,由於他們屬於不一樣的對象。這種狀況下,只容許一個同時有一個線程修改vacanciescinema1或者vacanciescinema2,可是容許有兩個線程同時修改vacanciescinema1和vacanciescinema2。

在同步塊中使用條件(wait(),notify(),notifyAll())

首先須要明確:
  1. 上述三個方法都是Object 類的方法。
  2. 上述三個方法都必須在同步代碼塊中使用。
當一個線程調用wait()方法時,JVM將這個線程置入休眠,而且釋放控制這個同步代碼塊的對象,同時容許其餘線程執行這個對象控制的其餘同步代碼塊。爲了喚醒這個線程,必須在這個對象控制的某個同步代碼塊中調用notify()或者notifyAll()方法。
上述一段話很重要!!!它說明了使用上述三個函數的方法以及方法的做用。
 
wait():將線程置入休眠狀態,而且釋放控制這個同步代碼塊的對象,釋放了之後其餘線程就能夠執行這個對象控制的其餘代碼塊。也就是能夠進入了。這個和Thread.sleep(millions)方法不一樣,sleep()方法是睡眠指定時間後自動喚醒。
notify()/notifyAll():使用wait()方法休眠的線程須要在該對象控制的某個同步代碼塊中 調用notify或者notifyAll()方法去喚醒,才能進入就緒狀態等待JVM的調用。不然一致處於休眠狀態。
難點:線程休眠和喚醒的時機,就是說何時調用notify()或者notifyAll()方法???
拿生產者和消費者的例子來講:生產者往隊列中塞數據,消費者從隊列中取數據,因此這個隊列是共享數據
數據存儲類 EventStorage
塞數據方法和取數據方法:set()、get()
複製代碼
 1 public synchronized void set(){
 2             while (storage.size()==maxSize){
 3                 try {
 4                     wait();
 5                 } catch (InterruptedException e) {
 6                     e.printStackTrace();
 7                 }
 8             }
 9             storage.add(new Date());
10             System.out.printf("Set: %d\n", storage.size());
11             notify();
12     }    
13    public synchronized void get(){
14             while (storage.size()==0){
15                 try {
16                     wait();
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20             }
21             System.out.printf("Get: %d: %s\n",storage.size(),((LinkedList<?>)storage).poll());
22             notify();
23     }
複製代碼
 
分析上面這個簡單的程序:
一、方法使用synchronized關鍵字聲明同步代碼塊。因此這個函數裏面可使用同步條件。
二、首先判斷隊列是否已經滿了,這裏要使用while而不是if。爲何呢?while是一致查詢是否已經滿了,而if是判斷一次就完事了。
三、若是滿了,調用wait()方法釋放該對象,那麼其餘方法(例如get())就可使用這個對象了。get()方法進入後取出一個數據,而後喚醒上一個被休眠的線程。
四、雖然線程被喚醒了,可是因爲get()方法線程佔用對象鎖,因此set()方法處於阻塞狀態。直到get()方法取出全部的數據知足休眠條件之後,set()方法從新執行
五、重複以上步驟

使用鎖實現同步

Java提供了同步代碼塊的另外一種機制,它比synchronized關鍵字更強大也更加靈活。這種機制基於Lock接口及其實現類(例如:ReentrantLock)
它比synchronized關鍵字好的地方:
一、提供了更多的功能。tryLock()方法的實現,這個方法試圖獲取鎖,若是鎖已經被其餘線程佔用,它將返回false並繼續往下執行代碼。
二、Lock接口容許分離讀和寫操做,容許多個線程讀和只有一個寫線程。ReentrantReadWriteLock
三、具備更好的性能
一個鎖的使用實例:
複製代碼
 1 public class PrintQueue {
 2     private final Lock queueLock=new ReentrantLock();
 3 
 4     public void printJob(Object document){
 5         queueLock.lock();
 6         
 7         try {
 8             Long duration=(long)(Math.random()*10000);
 9             System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),(duration/1000));
10             Thread.sleep(duration);
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         } finally {
14             queueLock.unlock();
15         }
16     }
17 }
複製代碼
聲明一把鎖,其中ReentrantLock(可重入的互斥鎖)是Lock接口的一個實現
1 private final Lock queueLock=new ReentrantLock();
而後在函數裏面調用lock()方法聲明同步代碼塊(臨界區)
1 queueLock.lock();
最後在finally塊中釋放鎖,重要!!!
1 queueLock.unlock();

使用讀寫鎖實現同步數據訪問

鎖機制最大的改進之一就是ReadWriteLock接口和他的惟一實現類ReentrantReadWriteLock.這個類有兩個鎖,一個是讀操做鎖,一個是寫操做鎖。使用讀操做鎖時能夠容許多個線程同時訪問,使用寫操做鎖時只容許一個線程進行。在一個線程執行寫操做時,其餘線程不可以執行讀操做。
 
在調用寫操做鎖時,使用一個線程。
寫操做鎖的用法:
複製代碼
1 public void setPrices(double price1, double price2) {
2         lock.writeLock().lock();
3         this.price1=price1;
4         this.price2=price2;
5         lock.writeLock().unlock();
6     }
複製代碼
讀操做鎖:
複製代碼
 1   public double getPrice1() {
 2         lock.readLock().lock();
 3         double value=price1;
 4         lock.readLock().unlock();
 5         return value;
 6     }
 7     public double getPrice2() {
 8         lock.readLock().lock();
 9         double value=price2;
10         lock.readLock().unlock();
11         return value;
12     }
複製代碼

修改鎖的公平性

ReentrantLock和ReetrantReadWriteLock構造函數都含有一個布爾參數fair。默認fair爲false,即非公平模式。
公平模式:當有不少線程在等待鎖時,鎖將選擇一個等待時間最長的線程進入臨界區。
非公平模式:當有不少線程在等待鎖時,鎖將隨機選擇一個等待區(就緒狀態)的線程進入臨界區。
這兩種模式只適用於lock()和unlock()方。而Lock接口的tryLock()方法沒有將線程置於休眠,fair屬性並不影響這個方法。

在鎖中使用多條件(Multri Condition)

鎖條件能夠和synchronized關鍵字聲明的臨界區的方法(wait(),notify(),notifyAll())作類比。鎖條件經過Conditon接口聲明。Condition提供了掛起線程和喚醒線程的機制。
使用方法:
複製代碼
 1 private Condition lines;
 2     private Condition space;
 3      */
 4     public void insert(String line) {
 5         lock.lock();
 6         try {
 7             while (buffer.size() == maxSize) {
 8                 space.await();
 9             }
10             buffer.offer(line);
11             System.out.printf("%s: Inserted Line: %d\n", Thread.currentThread()
12                     .getName(), buffer.size());
13             lines.signalAll();
14         } catch (InterruptedException e) {
15             e.printStackTrace();
16         } finally {
17             lock.unlock();
18         }
19     }
20 public String get() {
21         String line=null;
22         lock.lock();        
23         try {
24             while ((buffer.size() == 0) &&(hasPendingLines())) {
25                 lines.await();
26             }
27             
28             if (hasPendingLines()) {
29                 line = buffer.poll();
30                 System.out.printf("%s: Line Readed: %d\n",Thread.currentThread().getName(),buffer.size());
31                 space.signalAll();
32             }
33         } catch (InterruptedException e) {
34             e.printStackTrace();
35         } finally {
36             lock.unlock();
37         }
38         return line;
39     }
相關文章
相關標籤/搜索