《Java併發編程實戰》讀書筆記

Subsections 
線程安全(Thread safety)
鎖(lock)
共享對象
對象組合
基礎構建模塊
任務執行
取消和關閉
線程池的使用
性能與可伸縮性
併發程序的測試
顯示鎖
原子變量和非阻塞同步機制

一、線程安全(Thread safety)
無論何時,只要多於一個線程訪問給定的狀態變量。而且其中某個線程會寫入該變量,此時必須使用同步來協助線程對該變量的訪問。
線程安全是指多個線程在訪問一個類時,如果不需要額外的同步,這個類的行爲仍然是正確的。
線程安全的實例:
(1)、一個無狀態的類是線程安全的。
     無狀態類是指不包含任何域或也沒有引用其它類的域。一次特定計算的瞬間狀態,會唯一存在本地變量中。
(2)、原子操作是線程安全的。
     自增操作時一個離散操作的簡寫形式,獲取當前值,加一,寫回新值。這是一個讀-改-寫操作,不具備原子性。
(3)、競爭條件是不安全的。
     當計算的正確性依賴於運行時相關的時序或者多線程的交替時,會產生競爭條件。最常見的一種是檢查再運行(check-then-act)。
eg:
public class Instance() {
     private Instance in = null;
     public Instance getInstance() {
          if(in == null) {
              in= new Instance();
          }
          return in;
     }
}
這個例子意圖是想得到一個單例的對象,但是如果兩個線程同時執行到getInstance()這個地方,此時此刻,in是否爲null,這依賴於時序。這是無法預測到的。
解決檢查再運行和讀-改-寫操作導致的線程不安全方法,就是必須保證操作的原子性。而java 內置的原子性機制-鎖可以解決這些個問題。

二、鎖(lock)
(1)、內部鎖,java提供了強制原子性的內置鎖機制:synchronized 塊。
操作共享狀態的複合操作必須是原子的,以避免競爭條件,比如讀-改-寫操作和檢查再運行操作。複合操作會在完整的運行期佔有鎖,以確保其行爲爲原子的。

三、共享對象
要編寫正確的程序,關鍵問題在於:在訪問共享的可變狀態時需要進行正確的管理。
(1)、內存可見性
我們不僅希望防止某個線程在訪問某個狀態,而另外一個線程同時在修改這個狀態。同時希望確保一個線程修改了對象狀態以後,其它線程能夠看到發生的狀態變化。
在沒有同步的情況下,編譯器,處理器對操作的執行順序進行一些意想不到的調整。在缺少同步多線程程序中,要想對內存操作的執行順序進行判斷,幾乎無法得到正確的結論。

(2)、失效數據
在缺少同步的程序中產生錯誤的結果的一種情況就是,失效數據。

(3)、加鎖與可見性
鎖可以確保某個線程以一種可以預測的方式來查看另一個線程的執行結果,比如A執行某個同步代碼塊時,線程B隨後進入同一鎖保護的代碼塊。在B執行鎖保護的同步代碼塊時,可以看到線程A之前在同一代碼塊中所有的操作結果。
爲什麼在訪問某個共享且可變的共享變量時,要求線程在同一個鎖上同步,是爲了確保某個線程寫入該變量的值對於其它線程來說是可見的。否則,如果一個線程在未持有正確鎖的情況下讀取某個變量,那麼讀到的可能是一個失效值。

(4)、發佈和逸出

(5)、線程封閉
當訪問共享的可變數據時,通常需要同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步。這種技術稱爲線程封閉。它是線程安全最簡單方式之一。
eg:JDBC 的Connection對象,servlet請求大部分都是單線程同步方式處理,並且在Connection對象返回之前,連接池不會再將它分配給其它線程。
維持線程封閉更規範的方法是使用ThreadLocal
其設計的初衷是爲了解決多線程編程中的資源共享問題。提起這個,大家一般會想到synchronized,synchronized採取的是“以時間換空間”的策略,本質上是對關鍵資源上鎖,讓大家排隊操作。而ThreadLocal採取的是“以空間換時間”的思路,爲每個使用該變量的線程提供獨立的變量副本,在本線程內部,它相當於一個“全局變量”,可以保證本線程任何時間操縱的都是同一個對象。
原理:每個運行的線程都會有一個類型爲ThreadLocal.ThreadLocalMap的map,這個map就是用來存儲與這個線程綁定的變量,map的key就是ThreadLocal對象,value就是線程正在執行的任務中的某個變量的包裝類Entry.
ThreadLocal有四個方法:
//返回此線程局部變量的當前線程副本中的值
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

//如果是第一次調用,需要初始化。

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
//將此線程局部變量的當前線程副本中的值設置爲指定值

public void set(T value) {

        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

//移除此線程局部變量的值。

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
數據庫連接管理類,轉載:http://blog.csdn.net/ghsau/article/details/15732053

public class ConnectionManager {

     /** 線程內共享Connection,ThreadLocal通常是全局的,支持泛型 */
     private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
     
     public static Connection getCurrConnection() {
          // 獲取當前線程內共享的Connection
          Connection conn = threadLocal.get();
          try {
               // 判斷連接是否可用
               if(conn == null || conn.isClosed()) {
                    // 創建新的Connection賦值給conn(略)
                    // 保存Connection
                    threadLocal.set(conn);
               }
          } catch (SQLException e) {
               // 異常處理
          }
          return conn;
     }
     
     /**
      * 關閉當前數據庫連接
      */
     public static void close() {
          // 獲取當前線程內共享的Connection
          Connection conn = threadLocal.get();
          try {
               // 判斷是否已經關閉
               if(conn != null && !conn.isClosed()) {
                    // 關閉資源
                    conn.close();
                    // 移除Connection
                    threadLocal.remove();
                    conn = null;
               }
          } catch (SQLException e) {
               // 異常處理
          }
     }
}

(6)、不變性
滿足同步需求的另外一種方法是使用不可變對象。不可變對象一定是線程安全的。
不可變對象的定義:對象創建出來後(通過構造方法)無論如何對此對象進行非暴力操作(不用反射),此對象的狀態(實例域的值)都不會發生變化,那麼此對象就是不可變的,相應類就是不可變類,跟是否用 final 修飾沒關係。
在併發程序中使用和共享對象時,可以使用一下策略:
線程封閉。線程封閉的對象只能由一個線程擁有。
只讀共享。在沒有額外同步情況下,共享的只讀對象可以由多個線程併發訪問。
線程共享安全。線程安全的對象在其內部實現同步。因此多個線程可以通過對象的公有接口來訪問而不需要進一步的同步。

四、對象組合
(1)、如何設計線程安全的類。
     設計安全類需要注意一下三要素:
     找出構造對象狀態的所有變量。
     約束狀態變量的不變性條件。
     建立對象狀態的併發訪問管理策略。
(2)、實例封閉
     如果某對象不是線程安全的,我們可以通過多種技術使其在多線程中能安全的使用。確保該對象只能由單個線程訪問。
 public class PersonSet{
          private final Set<Person> mySet = new HashSet<Person>();
          
          public sychronized void addPersion(Person p) {
               mySet.add(p)
          }
     
          public sychronized boolean containsPerson(Person p) {
               return mySet.contains(p);
          }
     }
雖然HashSet 並非線程安全的。但是mySet是私有的不會逸出。唯一能訪問mySet的代碼是addPerson(),和containsPerson()。在執行上他們都要獲的PersonSet 上的鎖。PersonSet的狀態完全又它的內置鎖保護。所以
PersonSet是一個線程安全的類。
java 平臺的類庫有很多實例封閉的例子。比如一些基本的容器並非線程安全的,如ArrayList,HashMap。類庫提供的包裝方法,Collections.synchronizedList(list)、Collections.synchronizedMap(m)使得非線程安全的類可以在多線程中使用。
(3)、java 監視器模式
     把對象的所有可變狀態都封裝起來,並由對象自己的內部鎖來保護。
 public class privateLock {
          private final Object myLock = new Object();
     
          private int weight;
          
          void someMethod() {
               synchronized(myLock) {
                    //訪問weight
               }
          }
     }
使用私有鎖對象比使用對象的內置鎖有許多優點。私有鎖可以將鎖封裝起來,客戶代碼無法得到鎖。但客戶可以通過公有方法來訪問鎖。以便參與到同步策略中去。
監視器好比一做建築,它有一個很特別的房間,房間裏有一些數據,而且在同一時間只能被一個線程佔據,進入這個建築叫做"進入監視器",進入建築中的那個特別的房間叫做"獲得監視器",佔據房間叫做"持有監視器",離開房間叫做"釋放監視器",離開建築叫做"退出監視器".

如上圖所示,一個線程通過1號門進入Entry Set(入口區),如果在入口區沒有線程等待,那麼這個線程就會獲取監視器成爲監視器的owner,然後執行監視區域的代碼。如果在入口區中有其它線程在 等待,那麼新來的線程也會和這些線程一起等待。線程在持有監視器的過程中,有兩個選擇,一個是正常執行監視器區域的代碼,釋放監視器,通過5號門退出監視 器;還有可能等待某個條件的出現,於是它會通過3號門到Wait Set(等待區)休息,直到相應的條件滿足後再通過4號門進入重新獲取監視器再執行。

注意:當一個線程釋放監視器時,在入口區和等待區的等待線程都會去競爭監視器,如果入口區的線程贏了,會從2號門進入;如果等待區的線程贏了會從4 號門進入。只有通過3號門才能進入等待區,在等待區中的線程只有通過4號門才能退出等待區,也就是說一個線程只有在持有監視器時才能執行wait操作,處於等待的線程只有再次獲得監視器才能退出等待狀態。

五、基礎構建模塊
(1)、同步容器類。包括Vector和Hashtable。同步的封裝容器類由Collections.sychronizedXXX工廠方法創建。
     eg:synchronizedList,synchronizedMap(m)、synchronizedSet(s)
(2)、同步工具類。
     阻塞隊列(BlockingQueue(LinkedBlockingQueue,ArrayBlockingQueue,PriorityBlockingQueue,SynchronousQueue))不僅可以保存對象的容器,而且還可以協調生產者和消費者之間的控制流。
     信號量(Semaphore):用來控制同時訪問某個特定資源的操作數量。通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。Semaphore允許線程獲取許可, 未獲得許可的線程需要等待.這樣防止了在同一時間有太多的線程執行。Semaphore實現的功能就類似廁所有5個坑,假如有10個人要上廁所,那麼同時只能有多少個人去上廁所呢?同時只能有5個人能夠佔用,當5個人中 的任何一個人讓開後,其中等待的另外5個人中又有一個人可以佔用了。另外等待的5個人中可以是隨機獲得優先機會,也可以是按照先來後到的順序獲得機會,這取決於構造Semaphore對象時傳入的參數選項。單個信號量的Semaphore對象可以實現互斥鎖的功能,並且可以是由一個線程獲得了“鎖”,再由另一個線程釋放“鎖”,這可應用於死鎖恢復的一些場合。
 eg:模擬30輛車去泊車,而車位有10個的場景. 當車位滿時,出來一輛車,纔能有一輛車進入停車. 轉載http://mouselearnjava.iteye.com/blog/1921468
package my.concurrent.semaphore;

import java.util.concurrent.Semaphore;

public class Car implements Runnable {

     private final Semaphore parkingSlot;

     private int carNo;

     /**
     * @param parkingSlot
     * @param carName
     */
     public Car(Semaphore parkingSlot, int carNo) {
          this.parkingSlot = parkingSlot;
          this.carNo = carNo;
     }

     public void run() {

          try {
               parkingSlot.acquire();
               parking();
               sleep(300);
               parkingSlot.release();
               leaving();

          } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
          }

     }

     private void parking() {
          System.out.println(String.format("%d號車泊車", carNo));
     }

     private void leaving() {
          System.out.println(String.format("%d號車離開車位", carNo));
     }

     private static void sleep(long millis) {
          try {
               Thread.sleep(millis);
          } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();

          }
     }

}

package my.concurrent.semaphore;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class ParkingCars {

     private static final int NUMBER_OF_CARS = 30;

     private static final int NUMBER_OF_PARKING_SLOT = 10;

     public static void main(String[] args) {

          /*
          * 採用FIFO, 設置true
          */
          Semaphore parkingSlot = new Semaphore(NUMBER_OF_PARKING_SLOT, true);

          ExecutorService service = Executors.newCachedThreadPool();

          for (int carNo = 1; carNo <= NUMBER_OF_CARS; carNo++) {
               service.execute(new Car(parkingSlot, carNo));
          }

          sleep(3000);

          service.shutdown();

          /*
          * 輸出還有幾個可以用的資源數
          */
          System.out.println(parkingSlot.availablePermits() + " 個停車位可以用!");
     }

     private static void sleep(long millis) {
          try {
               Thread.sleep(millis);
          } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
          }
     }

}

運行結果:
1號車泊車 
4號車泊車 
9號車泊車 
2號車泊車 
8號車泊車 
10號車泊車 
3號車泊車 
12號車泊車 
14號車泊車 
6號車泊車 
2號車離開車位 
4號車離開車位 
6號車離開車位 
1號車離開車位 
9號車離開車位 
3號車離開車位 
5號車泊車 
8號車離開車位 
10號車離開車位 
11號車泊車 
7號車泊車 
12號車離開車位 
13號車泊車 
14號車離開車位 
16號車泊車 
17號車泊車 
20號車泊車 
19號車泊車 
18號車泊車 
15號車泊車 
5號車離開車位 
20號車離開車位 
18號車離開車位 
22號車泊車 
11號車離開車位 
7號車離開車位 
13號車離開車位 
15號車離開車位 
21號車泊車 
26號車泊車 
23號車泊車 
28號車泊車 
25號車泊車 
16號車離開車位 
27號車泊車 
17號車離開車位 
30號車泊車 
24號車泊車 
29號車泊車 
19號車離開車位 
25號車離開車位 
24號車離開車位 
22號車離開車位 
26號車離開車位 
28號車離開車位 
30號車離開車位 
21號車離開車位 
23號車離開車位 
27號車離開車位 
29號車離開車位 
10 個停車位可以用!

六:任務執行
1、在線程中執行任務
首先需要找出清晰的任務邊界,各個任務之間是相互獨立的。任務並不依賴其它任務的狀態、結果和邊界。獨立性有助於實現併發。在調度和負載均衡中實現更高的靈活性。在正常負載下,服務器應用程序應該表現良好的吞吐量和快速的響應性。
eg:主線程接受連接和處理請求之間不斷交替運行。當服務器正在處理請求的時候,新到來的連接必須等待處理完以後才能在此調用accept()。此種方式,服務器的資源利用率非常低。
class singleThreadWebServer {
      ServerSocket socket = new ServerSocket(80) ;
      while (true) {
             Socket connection = socket.accept();
            handleRequest(connection);
      }
}
改進:
class MultiThreadWebServer {
      ServerSocket socket = new ServerSocket(80) ;
      while (true) {
             final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                   public void run() {
                        handleRequest(connection);
                  }
             }
             new Thread(task).start();
      }
}
結論:任務處理從主線程分離出來。使住線程能夠在完全前面的請求之前可以接受新的請求,從而提高響應性。
      任務可以並行處理,從而同時服務多個請求,如果有多個處理器,或者某個任務某種原因被阻塞,程序的吞吐量提高。
2、Executor 框架
先給一張java.util.concurrent 的結構

任務是一組邏輯工作單元,而線程則是使任務異步執行的邏輯單元。Executor爲靈活且強大的異步任務執行框架提供了基礎,還提供了對生命週期的支持,以及統計信息、應用管理機制和性能監視等機制。
Executor 基於生產者-消費者模式。提交任務相當是生產者,執行任務相當是消費者

a、執行策略:
任務在什麼(What)線程中執行
任務以什麼(What)順序執行(FIFO/LIFO/優先級等)
同時有多少個(How Many)任務併發執行
允許有多少個(How Many)個任務進入執行隊列
系統過載時選擇放棄哪一個(Which)任務,如何(How)通知應用程序這個動作
任務執行的開始、結束應該做什麼(What)處理

b、線程池:
線程池和工作者隊列密切相關,工作者線程的任務:從工作隊列中獲取一個任務,執行任務,然後返回線程池並等待下一個任務。
Executors類裏面提供了一些靜態工廠,生成一些常用的線程池。
newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
newSingleThreadScheduledExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。

c、線程池Executor任務拒絕策略
java.util.concurrent.RejectedExecutionHandler描述的任務操作。
第一種方式直接丟棄(DiscardPolicy)
第二種丟棄最舊任務(DiscardOldestPolicy)
第三種直接拋出異常(AbortPolicy)
第四種任務將有調用者線程去執行(CallerRunsPolicy)

d、生命週期
java.util.concurrent.ExecutorService 接口對象來執行任務,該接口對象通過工具類java.util.concurrent.Executors的靜態方法來創建。 Executors此包中所定義的 Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 類的工廠和實用方法。
ExecutorService擴展了Executor並添加了一些生命週期管理的方法。一個Executor的生命週期有三種狀態,運行 ,關閉 ,終止。Executor創建時處於運行狀態。當調用ExecutorService.shutdown()後,處於關閉狀態,isShutdown()方法返回true。這時,不應該再想Executor中添加任務,所有已添加的任務執行完畢後,Executor處於終止狀態,isTerminated()返回true。
shutdown():執行平緩的關閉過程,不再接受新的任務,同時等待已經提交的任務執行完成。
shutdownNow();執行粗暴的關閉過程,嘗試取消所有運行中的任務,並且不再啓動隊列中尚未開始啓動的任務。
awaitTermination: 這個方法有兩個參數,一個是timeout即超時時間,另一個是unit即時間單位。這個方法會使線程等待timeout時長,當超過timeout時間後,會監測ExecutorService是否已經關閉,若關閉則返回true,否則返回false。一般情況下會和shutdown方法組合使用。
// 創建一個固定大小的線程池
 ExecutorService service = Executors. newFixedThreadPool(3);
          for ( int i = 0; i < 10; i++) {
              System. out.println( "創建線程" + i);
              Runnable run = new Runnable() {
                  @Override
                  public void run() {
                      System. out.println( "啓動線程");
                  }
              };
              // 在未來某個時間執行給定的命令
              service.execute(run);
          }
          // 關閉啓動線程
          service.shutdown();
          // 每隔1秒監測一次ExecutorService的關閉情況.
          service.awaitTermination(1, TimeUnit. SECONDS);
          System. out.println( "all thread complete");
          System. out.println(service.isTerminated());
3、攜帶結果的任務callable 和Future

七:取消和關閉
要想使任務和線程安全、快速、可靠的停下來,並不是件容易的事情,java沒有提供任何機制來安全終止線程。但他提供了中斷(Interruption),一種協作機制,能使線程終止另外一個線程的當前工作。
任務取消
中斷
中斷策略
響應中斷

八、線程池的使用

九、避免活躍性危險
我們使用加鎖機制來確保線程安全,但如果過度使用,會導致順序死鎖(Lock-Ordering Deadlock)。我們使用線程池和信號量來限制對資源的限制。但這些被限制的行爲可能導致資源死鎖(Resource Deadlock)。
死鎖:每個人擁有其他人需要的資源,同時等待其他人已經擁有的資源,並且每個人在獲取所有需要的資源之前都不放棄已經擁有的資源。
飢餓:當線程無法訪問到它所需要的資源而不能繼續執行時,就會發生飢餓。引發飢餓的最常見資源就是CUP的時鐘週期。
活鎖:liveLock。改問題儘管不會阻塞線程,但也不能繼續執行,因爲線程將不斷重複執行相同的操作,而且總會失敗。

十、性能與可伸縮性
可伸縮性:當增加計算資源時,(CPU,內存,存儲容量和I/O寬帶),程序的吞吐量和處理能力響應的增加。
吞吐量:指一組併發任務中已完成任務所佔的比例。
響應性:指請求從發出到完成所需要的時間。
引入線程存在的開銷:
(1)、上下文切換。cpu在做線程切換的時候,需要保存當前線程執行的上下文,並且新調度進來的線程執行上下文設置爲當前上下文。發生越多的上下文切換,增加了調度開銷,並因此降低吞吐量。
(2)、內存同步。synchronized 發生鎖競爭的地方帶來的開銷會影響其它線程的性能。
(3)、阻塞。當在鎖上發生競爭時,競爭失敗的線程會阻塞,JVM 通過循環,不斷的嘗試獲取鎖,直到成功。或者通過操作系統掛起阻塞的線程。如果時間短,採用等待方式,如果時間長才適合採用線程掛起的方式。
串行操作降低可伸縮性,並行切換上下文也會降低性能。在鎖發生競爭時,會同時導致上面兩種問題,因此,減少鎖的競爭能夠提高性能和收縮性。在併發程序中,對可伸縮性最主要的威脅就是獨佔方式的資源鎖。
兩個因素將影響鎖上面發生競爭的可能性:鎖的請求頻率以及每次持有該鎖的時間。如果兩者的乘積很小,那麼大多數獲取鎖操作都不會發生競爭。
三種方式可以降低鎖的競爭程度:
(1)、降低鎖的請求頻率。
降低線程請求鎖的頻率,可以通過鎖分解和鎖分段等技術來實現。即減小鎖的粒度。如果一個鎖同時需要保護好幾個狀態變量,那麼可以把這個鎖分解成多個鎖,並且每個鎖只保護一個狀態變量,從而提高可伸縮性,並最終降低每個鎖的請求頻率。但是使用的鎖越多,發生死鎖的風險也會越高。

(2)、減少鎖的持有時間。
縮小鎖的範圍(快進快出),可以將一些與鎖無關的代碼移出同步代碼塊,尤其是開銷較大的操作,以及可能被阻塞的操作,比如I/O 操作。

(3)、放棄使用獨佔鎖,併發容器,讀-寫鎖,不可變對象以及原子變量,

十一、併發程序的測試

十二、顯示鎖
訪問共享對象可以使用的機制有synchronized,volatile,ReentrantLock。
有了synchronized 爲啥JSR 166 小組花了這麼多時間來開發 java.util.concurrent.lock 框架呢?答案很簡單-同步是不錯,但它並不完美。它有一些功能性的限制 —— 它無法中斷一個正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進行,多數情況下,這沒問題(而且與異常處理交互得很好),但是,確實存在一些非塊結構的鎖定更合適的情況。
Lock 和ReentrantLock
Lock接口中定義了一組抽象的加鎖操作。Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖後去操作。ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。在確實需要一些 synchronized 所沒有的特性的時候,比如時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者鎖投票。
格式:
Lock lock = new ReentrantLock();
lock.lock();
try {
  // update object state
}
finally {
  lock.unlock();
}
必須在finally 中來釋放Lock。

定時鎖和輪詢鎖是爲了避免死鎖的發生。如果不能獲取所需要的鎖,可以使用定時的或者輪詢的鎖獲取方式,從而使你重新獲的控制權。
(1)、輪詢鎖( tryLock())
(2)、定時鎖(tryLock(timeout, NANOSECONDS))。如果操作不能在指定的時間內給出結果,那麼就會使程序提前結束
(3)、定時以及可中斷的鎖(lockInterruptibly)
(4)、讀寫鎖(ReadWriteLock)。可以被多個讀者訪問或者被一個寫者訪問。
ReentrantLock 每次只能一個線程訪問加鎖的數據,從而達到維護數據完整性的目的。通過這種策略可以避免寫/寫和寫/讀衝突。但是也同時避免了讀/讀衝突。但是有些時候讀操
作是可以被併發進行的。所以需要讀寫鎖。
eg:實現一個簡單的緩存。

十三、原子變量和非阻塞同步機制
(1)、原子變量 
大多數現代處理器都包含對多處理的支持。當然這種支持包括多處理器可以共享外部設備和主內存,同時它通常還包括對指令系統的增加來支持多處理的特殊要求。特別是,幾乎每個現代處理器都有通過可以檢測或阻止其他處理器的併發訪問的方式來更新共享變量的指令。
現在的處理器(包括 Intel 和 Sparc 處理器)使用的最通用的方法是實現名爲“比較並交換(Compare And Swap)”或 CAS 的原語。
CAS 操作包含三個操作數—— 內存位置(V)、預期原值(A)和新值(B)。通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新值 B,然後使用 CAS 將 V 的值從 A 改爲 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。
類似於 CAS 的指令允許算法執行讀-修改-寫操作,而無需害怕其他線程同時修改變量,因爲如果其他線程修改變量,那麼 CAS 會檢測它(並失敗),算法可以對該操作重新計算。
eg:模擬CAS來實現一個計數器。
public class CASCount implements Runnable {
	
	private SimilatedCAS counter = new SimilatedCAS();  

	@Override
	public void run() {
		 for (int i = 0; i < 10000; i++) {  
	            System.out.println(this.increment());  
	     }  
	}
	
	public int increment() {
		int oldValue = counter.getValue();
		int newValue = oldValue + 1;
		
		while (!counter.compareAndSwap(oldValue, newValue)) { //如果CAS失敗,就去拿新值繼續執行CAS
			  oldValue = counter.getValue();  
	          newValue = oldValue + 1;  
		}
		
		return newValue;
	}
	
	public static void main(String[] args) {  
        Runnable run = new CASCount();  
  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
    }  

}

class SimilatedCAS {
	private int value;
	
	public int getValue() {
		return value;
	}
	
	// 這裏只能用synchronized了,畢竟無法調用操作系統的CAS  
    public synchronized boolean compareAndSwap(int expectedValue, int newValue) {  
        if (value == expectedValue) {  
            value = newValue;  
            return true;  
        }  
          
        return false;  
    }  
}

JDK 5.0引入底層CAS支持,java.util.concurrenent.atomic.AtomicXXX,使用底層的JVM支持爲數字和引用類型提供一種高效的CAS操作。
eg:AtomicInteger,AtomicIntegerArray,AtomicLong,AtomicLongArray
原子變量能夠支持原子的有條件的,讀-改-寫操作。
eg:使用原子變量類 實現一個計數器。
public class AtomicCounter implements Runnable{
	
	//AtomicInteger採用了系統的CAS
	private AtomicInteger value = new AtomicInteger();  

	@Override
	public void run() {
		for (int i = 0; i < 10000; i++) {  
            System.out.println(value.incrementAndGet());  
        }  
	}
	
	public static void main(String[] args) {  
        Runnable run = new AtomicCounter();  
  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
        new Thread(run).start();  
    }  

}

(1)、非阻塞算法提供比synchronized 機制更高的性能和可收縮性。可以使多個線程在競爭相同的數據時候不會發生阻塞。
基於鎖的算法中可能會出現各種活躍性的障礙,比如I/O 阻塞,導致其它線程都無法進行下去,如果某種算法中,一個線程的失敗或者掛起,不會影響其它線程的失敗或者掛起,那麼這種算法就是非阻塞算法。
(1)、非阻塞棧(鏈式存儲結構)
push方法創建一個新的節點,改節點的next域指向當前棧頂。讓後使用CAS把這個新節點放入棧頂。如果在開始插入的時候位於棧頂的節點沒有變化則CAS成功,如果棧頂發生了變化(其它線程操作引起)那麼CAS會失敗。
eg:ConcurentStack.java
public class ConcurrentStack {
	
	class Node {
		public final int item;
		public Node next;
		public Node(int item) {
			this.item = item;
		}
	}
	
	private AtomicReference<Node> top = new AtomicReference<ConcurrentStack.Node>();
	
	//入棧
	public void push(int item) {
		Node newNode = new Node(item);
		Node oldHead;
		do {
			oldHead = top.get();
			newNode.next = oldHead;
		} while (!top.compareAndSet(oldHead, newNode));
	}
	
	//出棧
	public int pop() {
		Node oldHead;
		Node newHead;
		do {
			oldHead = top.get();
			if(oldHead == null ) {
				return -1;
			}
			newHead = oldHead.next; 
			//Atomically sets the value to the given updated value if the current value {@code ==} the expected value.
		} while (!top.compareAndSet(oldHead, newHead));
		return oldHead.item;
	}

}









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章