1. 程式人生 > >(一)juc線程高級特性

(一)juc線程高級特性

線程 main方法 exec jdk iterator 不同的 另一個 創建 ron

1. volatile 關鍵字與內存可見性

  內存可見性(Memory Visibility)是指當某個線程正在使用對象狀態而另一個線程在同時修改該狀態,需要確保當一個線程修改了對象狀態後,其他線程能夠看到發生的狀態變化。

  可見性錯誤是指當讀操作與寫操作在不同的線程中執行時,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。

定義線程類ThreadDemo,功能是將boolean型變量flag的值修改

class ThreadDemo implements Runnable {

    private volatile boolean
flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() {
return flag; } public void setFlag(boolean flag) { this.flag = flag; } }

測試:線程1修改ThreadDemo類中 flag的值(由false修改為true),線程main方法判斷 flag的值,當為true時輸出信息,並停止

public class TestVolatile {
    //線程main方法判斷boolean值,當為true時輸出“------------” 並停止線程
    public static void
main(String[] args) { ThreadDemo td = new ThreadDemo(); //線程1為ThreadDemo的run方法,將boolean值由false改為true new Thread(td).start(); while(true){ if(td.isFlag()){ System.out.println("------------------"); break; } } } }

運行如下:

技術分享圖片

即由於可見性錯誤導致線程main方法讀取到主存中的flag值並不是線程1修改後的值(讀取操作發生在線程1將flag值寫入主存之前)。

解決方法:

(1)使用同步鎖synchronized,讓線程main重復的到主存中讀取數據,但是當多線程操作時,效率很低

synchronized (td) {
     if(td.isFlag()){
          System.out.println("------------------");
            break;
       } }                   

(2)使用volatile 量,用來確保將變量的更新操作通知到其他線程

private volatile boolean flag = false;

可以將 volatile 看做一個輕量級的鎖,但是又與鎖有些不同:

  • 對於多線程,不是一種互斥關系
  • 不能保證變量狀態的“原子性操作”

2. 原子變量與 CAS 算法

以 i++為例,i++操作實際上分為三個步驟

//i++ 的原子性問題:
   int i = 10;
   i = i++; //10
//i++ 的操作實際上分為三個步驟“讀-改-寫”
   int temp = i;
   i = i + 1;
   i = temp;

測試如下:

public class TestAtomicDemo {
    public static void main(String[] args) {
        AtomicDemo ad = new AtomicDemo();
    //創建10個線程對serialNumber值進行增加操作        
        for (int i = 0; i < 10; i++) {
            new Thread(ad).start();
        }
    }    
}

class AtomicDemo implements Runnable{
    //聲明volatile變量
    private volatile int serialNumber = 0;
   
    @Override
    public void run() {       
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }        
        System.out.println(getSerialNumber());
    }
    //對變量進行i++操作
    public int getSerialNumber(){
        return serialNumber++;
    }        
}

可能會出現如下結果:

技術分享圖片

原因為 volatile只能保證內存可見性,但是線程1和線程2中進行的 i++操作卻分好幾個步驟,即不能保證變量狀態的“原子性操作”。

  技術分享圖片

解決方法:

使用原子變量(在 java.util.concurrent.atomic 包下)

//    private volatile int serialNumber = 0;
private AtomicInteger serialNumber = new AtomicInteger(0);

public int getSerialNumber(){
//      return serialNumber++;
        return serialNumber.getAndIncrement();
    }

以 AtomicInteger分析原子變量的實現

(1)volatile 保證內存可見性

  技術分享圖片

(2)CAS(Compare-And-Swap) 算法保證數據變量的原子性

  技術分享圖片

CAS (Compare-And-Swap) 是一種硬件對並發的支持,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享數據的並發訪問。

CAS 是一種無鎖的非阻塞算法的實現。

CAS 包含了 3 個操作值:

  • 需要讀寫的內存值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V 的值,否則不會執行任何操作。

可通過源碼查看sun.misc.Unsafe.class中的getAndAddInt方法實現來理解CAS算法

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
   {
     int i;
do
     {
       i = getIntVolatile(paramObject, paramLong);
     } while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
     return i;
   }

3. 模擬CAS算法

/*
 * 模擬 CAS 算法
 */
public class TestCompareAndSwap {
    public static void main(String[] args) {
        final CompareAndSwap cas = new CompareAndSwap();      
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {            
                @Override
                public void run() {
                    int expectedValue = cas.get();
                    boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
                    System.out.println(b);
                }
            }).start();
        }        
    }    
}

class CompareAndSwap{
    private int value;    
    //獲取內存值
    public synchronized int get(){
        return value;
    }    
    //比較
    public synchronized int compareAndSwap(int expectedValue, int newValue){
        int oldValue = value;      
        if(oldValue == expectedValue){
            this.value = newValue;
        }      
        return oldValue;
    }    
    //設置
    public synchronized boolean compareAndSet(int expectedValue, int newValue){
        return expectedValue == compareAndSwap(expectedValue, newValue);
    }
}

4. 同步容器類 ConcurrentHashMap

Java 5.0 在 java.util.concurrent 包中提供了多種並發容器類來改進同步容器的性能。

ConcurrentHashMap 同步容器類是Java 5 增加的一個線程安全的哈希表。對與多線程的操作,介於 HashMap 與 Hashtable 之間。內部采用“鎖分段”機制(可理解為“並行”)替代 Hashtable 的獨占鎖(相當於“串行”),進而提高性能。

註意:jdk1.8之後ConcurrentHashMap底層采用的 CAS算法 取代“鎖分段”機制。

此包還提供了設計用於多線程上下文中的 Collection 實現:

ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。

  • 當期望許多線程訪問一個給定 collection 時,ConcurrentHashMap 通常優於同步的 HashMap,ConcurrentSkipListMap 通常優於同步的 TreeMap。
  • 當期望的讀數和遍歷遠遠大於列表的更新數時,CopyOnWriteArrayList 優於同步的 ArrayList。
/*
 * CopyOnWriteArrayList/CopyOnWriteArraySet : “寫入並復制”
 * 註意:添加操作多時,效率低,因為每次添加時都會進行復制,開銷非常的大。並發叠代操作多時可以選擇。
 */
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        HelloThread ht = new HelloThread();        
        for (int i = 0; i < 10; i++) {
            new Thread(ht).start();
        }
    }    
}

class HelloThread implements Runnable{
    
// 使用ArrayList,報錯java.util.ConcurrentModificationException
//    private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
    
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();    
    static{
        list.add("AA");
        list.add("BB");
        list.add("CC");
    }

    @Override
    public void run() {       
        Iterator<String> it = list.iterator();        
        while(it.hasNext()){
            //叠代和add方法操作同一個數據源
            System.out.println(it.next());
            list.add("AA");
        }        
    }    
}

5. CountDownLatch 閉鎖

Java 5.0 在 java.util.concurrent 包中提供了多種並發容器類來改進同步容器的性能。

CountDownLatch 一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。

閉鎖可以延遲線程的進度直到其到達終止狀態,閉鎖可以用來確保某些活動直到其他活動都完成才繼續執行:

  • 確保某個計算在其需要的所有資源都被初始化之後才繼續執行;
  • 確保某個服務在其依賴的所有其他服務都已經啟動之後才啟動;
  • 等待直到某個操作所有參與者都準備就緒再繼續執行。

如下:要求在創建的5個線程都執行完畢之後,再調用線程main方法輸出耗費時間,使用wait,notify和notifyAll方法也可實現,但JDK不推薦。這裏使用CountDownLatch。

/*
 * CountDownLatch :閉鎖,在完成某些運算時,只有其他所有線程的運算全部完成,當前運算才繼續執行
 */
public class TestCountDownLatch {

    public static void main(String[] args) {
        //CountDownLatch的構造函數接收一個int類型的參數作為計數器,如果你想等待N個點完成,這裏就傳入N。
        final CountDownLatch latch = new CountDownLatch(5);
        LatchDemo ld = new LatchDemo(latch);

        long start = System.currentTimeMillis();

        for (int i = 0; i < 5; i++) {
            new Thread(ld).start();
        }

        try {
            //阻塞當前線程,直到N變成零。
            latch.await();
        } catch (InterruptedException e) {
        }

        long end = System.currentTimeMillis();

        System.out.println("耗費時間為:" + (end - start));
    }

}

class LatchDemo implements Runnable {

    private CountDownLatch latch;
    public LatchDemo(CountDownLatch latch) {
        this.latch = latch;
    }
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                if (i % 2 == 0) {
                    System.out.println(i);
                }
            }
        } finally {  //計數器減一
            latch.countDown();
        }
    }
}

結果:

技術分享圖片

6. 創建執行線程的方式三:實現 Callable 接口

Thread類和Runnable接口都不允許聲明檢查型異常,也不能定義返回值。Thread類和Runnable接口都不允許聲明檢查型異常,也不能定義返回值。

不能聲明拋出檢查型異常這個問題比較麻煩。public void run()方法契約意味著你必須捕獲並處理檢查型異常。即使你小心地保存了異常信息(在捕獲異常時)以便稍後檢查,但也不能保證這個類(Runnable對象)的所有使用者都讀取異常信息。你也可以修改Runnable實現的getter,讓它們都能拋出任務執行中的異常。但這種方法除了繁瑣也不是十分安全可靠,你不能強迫使用者調用這些方法,程序員很可能會調用join()方法等待線程結束然後就不管了。

Java 5.0 java.util.concurrent 提供了一個新的創建執行線程的方式:Callable 接口。Callable接口定義了方法public T call() throws Exception。我們可以在Callable實現中聲明強類型的返回值,甚至是拋出異常。

Callable 需要依賴FutureTask ,FutureTask 也可以用作閉鎖。

/*
 * 一、創建執行線程的方式三:實現 Callable 接口。 相較於實現 Runnable 接口的方式,方法可以有返回值,並且可以拋出異常。
 * 
 * 二、執行 Callable 方式,需要 FutureTask 實現類的支持,用於接收運算結果。  FutureTask 是  Future 接口的實現類
 */
public class TestCallable {
    
    public static void main(String[] args) {
        ThreadDemo1 td = new ThreadDemo1();
        
        //1.執行 Callable 方式,需要 FutureTask 實現類的支持,用於接收運算結果。
        FutureTask<Integer> result = new FutureTask<>(td);
        
        new Thread(result).start();
        
        //2.接收線程運算後的結果
        try {
            Integer sum = result.get();  //FutureTask 可用於 閉鎖
            System.out.println(sum);
            System.out.println("------------------------------------");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class ThreadDemo1 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum = 0;        
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }    
        return sum;
    }  
}

/*class ThreadDemo implements Runnable{
    @Override
    public void run() {
    }    
}*/

7. 同步鎖 Lock

在 Java 5.0 之前,協調共享對象的訪問時可以使用的機制只有 synchronized 和 volatile 。Java 5.0 後增加了一些新的機制,但並不是一種替代內置鎖的方法,而是當內置鎖不適用時,作為一種可選擇的高級功能。

ReentrantLock 實現了 Lock 接口,並提供了與synchronized 相同的互斥性和內存可見性。但相較於synchronized 提供了更高的處理鎖的靈活性。

/*
 * 用於解決多線程安全問題的方式:
 * 
 * synchronized:隱式鎖
 * 1. 同步代碼塊
 * 
 * 2. 同步方法
 * 
 * jdk 1.5 後:
 * 3. 同步鎖 Lock
 * 註意:是一個顯示鎖,需要通過 lock() 方法上鎖,必須通過 unlock() 方法進行釋放鎖
 */
public class TestLock {    
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        
        new Thread(ticket, "1號窗口").start();
        new Thread(ticket, "2號窗口").start();
        new Thread(ticket, "3號窗口").start();
    }
}

class Ticket implements Runnable{  
    private int tick = 100; 
private Lock lock = new ReentrantLock();
@Override
public void run() { while(true){ lock.lock(); //上鎖 try{ if(tick > 0){ try { Thread.sleep(200); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " 完成售票,余票為:" + --tick); } }finally{ lock.unlock(); //釋放鎖 } } } }

(一)juc線程高級特性