4 鎖的優化及注意事項

「鎖」是最經常使用的同步方法之一。前端

有助於提升「鎖」性能的幾點建議

減小鎖持有的時間

public synchronized void syncMethod(){
  othercodel();
  mutextMethod();   //1
  othercode2();
}

在上面代碼中,若只有1處方法須要同步,而其餘兩個方法又是兩個重量級的方法。那麼整個syncMethod()方法在同步的時長上就會大大增長。若這個時候併發量較大,使用整個方法作同步就會致使等待線程大大增長。java

一個較爲好的解決方案是:只在必要時進行同步。這樣能夠減小線程持有鎖的時間,提升系統的吞吐量。以下面代碼:程序員

public void syncMethod2(){
  othercodel();
    synchronized(this){
        mutextMethod(); 
    }
  othercode2();
}

注意:減小鎖的持有時間有助於下降鎖衝突的可能性,進而提高系統的併發能力。算法

減小鎖粒度

所謂減小鎖粒度,就是指縮小鎖定對象的範圍,從而減小鎖衝突的可能性,進而提升系統的併發能力。數據庫

ConcurrentHashMap來講明減小鎖粒度api

它內部沒有對整個對象進行加鎖,而是在內部進一步細分了若干個小的HashMap,稱之爲段(默認分爲16段)。數組

若是須要再ConcurrentHashMap中增長一個新的表項,並非將整個HashMap加鎖,而是首先根據Hashcode獲得該表項應該被放在哪一個段中,而後對該段加鎖,並完成put()操做。在多線程下,只要被加入的表項不在同一個段中,線程間就能夠作到真正的並行。安全

因爲默認有16個段,夠幸運的話,ConcurrentHashMap能夠接受16個線程同時插入。大體以下圖:數據結構

減少鎖粒度_2019-09-09_10-47-26

可是有一個新問題。當系統須要取得全局鎖時,其消耗的資源較多。依然以ConcurrentHashMap爲例,它的put()方法分離了鎖,可是當試圖訪問ConcurrentHashMap的全局信息時,就要同時得到全部段的鎖方能順利實施。好比ConcurrentHashMap的size()方法,size()方法的部分代碼以下:多線程

size部分代碼_2019-09-09_10-24-04

不過,size方法不老是這樣執行,它會先使用無鎖的方式,若是失敗,纔會嘗試這種加鎖的方式。

讀寫分離鎖替換獨佔鎖

讀寫分離鎖替換獨佔鎖是減少鎖粒度的一種特殊狀況。若是說上節中提到的減小鎖粒度是經過分割數據結構實現的,那讀寫鎖則是對系統功能點的分割。

在讀多少寫的場合,使用讀寫分離鎖能夠有效提高系統的併發量

鎖分離

以LinkedBlockingQueue爲例來講明鎖分離。

take()和put()兩個操做分別做用於隊列的前端和尾端,看起來二者並不衝突,可是若二者使用獨佔鎖,那它們不可能實現真正的併發,在運行時,它們會互相等待對方釋放資源。在併發激烈的環境中,會影響性能。

所以,在JDK的實現中,取而代之的是兩把不一樣的鎖,分離了take()和put()操做。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

上面源碼中,定義了takeLock和putLock,它們分別在take()和put()操做中使用。這樣take()和put()就不存在鎖競爭的關係,只是在put()與put()、take()與take()間分別存在鎖競爭。從而,消弱了鎖競爭的可能。

take()方法:

taike()_2019-09-09_11-43-02

put()方法:

put_2019-09-09_11-43-47

鎖粗化

一般狀況下,一般每一個線程持有的鎖的時間都儘可能短,即便用完公共資源後,就應該當即釋放,這樣,等待在鎖上的其餘線程才能得到鎖資源執行任務。可是,這有一個度,若是對同一個鎖不停地請求、同步釋放,其自己也會消耗資源。

爲此,虛擬機在對不斷進行請求和釋放的操做時,會整合成對鎖的一次請求,從而減小對鎖的請求同步次數。這就叫鎖粗化,好比下列代碼:

鎖粗化_2019-09-09_13-52-51

會整合成:

鎖粗化_2019-09-09_13-53-11

尤爲是在循環時,以下,沒詞循環都是對鎖進行一次申請和釋放:

鎖粗化_2019-09-09_13-54-59

上面代碼應改爲:

鎖粗化_循環_2019-09-09_13-55-16

java虛擬機對鎖優化所作的努力

鎖偏向

若是一個線程得到了鎖,那麼鎖就進入了偏向模式。當這個線程再次請求鎖時,無需再作任何同步操做。這樣就節省了大量有關鎖申請的操做,提升了程序性能。

這種模式適合集合沒有鎖競爭的場合,若鎖競爭激烈,模式相似於失效。使用java虛擬機參數-XX:+UserBiasedLocking能夠開啓偏向鎖。

輕量級鎖

若偏向鎖失敗,虛擬機不會當即掛起線程。它還會使用一種稱爲輕量級鎖的優化手段。輕量級鎖只是簡單地將對象頭部做爲指針,指向持有鎖的線程堆棧的內部,來判斷一個線程是否持有對象鎖。若是線程得到輕量鎖成功,則能夠順利進入臨界區。若是輕量級鎖加鎖失敗,則表示其餘線程搶險爭奪到了鎖,那麼當前線程的鎖請求就會膨脹爲重量級鎖。

自旋鎖

鎖膨脹後,虛擬機作最後努力——自旋鎖。因爲當前線程暫時沒法得到鎖,可是何時能夠得到鎖時一個未知數。也許幾個CPU後就能夠從新得到鎖。若這時粗暴地掛起線程多是一種得不償失的操做。所以,系統會進行一次賭注:假設線程在短期內能再次得到這把鎖。所以,虛擬機會讓當前線程作幾個空循環(這也是自旋的含義),若能獲得,則進入臨界區。若不能得到,則真正在操做系統層面掛起。

鎖消除

鎖消除是一種更完全的鎖優化。Java虛擬機在JIT編譯時,經過對運行上下文的掃描,去除不能存在共享資源競爭的鎖。經過鎖消除,能夠節省毫無心義的請求鎖時間。

那若是是不可能存在鎖競爭,爲何程序員要加上鎖呢?

當咱們使用到JDK的一些內置API時,好比StringBuffer、Vector等,內部實現就會有鎖。

鎖消除涉及的一項關鍵技術是逃逸分析。

逃逸分析必須在-server模式下進行,可使用-XX:+DoEscapeAnalysis參數打開逃逸分析。使用-XX:+EliminateLocks參數能夠打開鎖消除。

人手一支筆:ThreadLocal

若是說鎖是一人一支筆,那麼ThreadLocal就是一人一支筆的思路了。

ThreadLocal的簡單使用

下面代碼中,咱們使用ThreadLocal爲每個線程都產生一個SimpleDateformat對象實例:

public class ThreadLocalDemo2 {
   static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    public static class ParseDate implements Runnable{
       int i=0;
       public ParseDate(int i){this.i=i;}
      public void run() {
         try {
            if(tl.get()==null){     //1
               tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }                       //2
            Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
            System.out.println(i+":"+t);
         } catch (ParseException e) {
            e.printStackTrace();
         }
      }
    }
   public static void main(String[] args) {
      ExecutorService es=Executors.newFixedThreadPool(10);
      for(int i=0;i<1000;i++){
         es.execute(new ParseDate(i));
      }
   }
}

上面1~2處,若是當前線程不持有SimpleDateformat對象實例。那麼就新建一個並把它設置到當前線程,若是已經持有,則直接使用。

從這裏也能夠看到,爲每個線程人手分配一個對象的工做並非由ThreadLocal來完成的,而是須要在應用層面保證。ThreadLocal只是起到了簡單的容器做用。

ThreadLocal的實現原理

ThreadLocal是如何保證這些對象只被當前線程所訪問呢?

咱們首先關注的是set()和get()方法。

先說set():

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

在set時,首先得到當前線程對象,而後經過getMap()拿到當前線程的ThreadLocalMap,並將值設如ThreadLocalMap中。而ThreadLocalMap能夠理解爲一個Map(雖然不是,可是能夠簡單理解成HashMap)。其中,key爲ThreadLocal當前對象,value就是咱們須要的值。而ThreadLocals自己就保存了當前本身所在線程的全部「局部變量」,也就是一個ThreadLocal變量的集合。

get():

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

在get()操做時,天然就是把map中的數據拿出來。首先,get()方法先取得當前線程的ThreadLocalMap對象。而後,經過將本身做爲key取得內部的實際數據。

釋放ThreadLocal:ThreadLocal.remove()防止內存泄露:

當線程退出時,Thread類會進行一些清理工做,其中就包括清理ThreadLocalMap。可是若是咱們使用線程池,那就意味着線程未必會退出。若是這樣,將一些大大的對象設置到ThreadLocal中(實際保存在線程持有的threadLocal Map中),可能會使系統出現內存泄露的可能(它沒法被回收)。因此,若是但願及時回收對象,最好使用ThreadLocal.remove()將這個變量移除。防止內存泄露。

也能夠像普通變量同樣釋放ThreadLocal。ThreadLocal = null

public class ThreadLocalDemo_Gc {
    static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected void finalize() throws Throwable { //重載了finalize() 當對象在GC時,打印信息
            System.out.println(this.toString() + " is gc");
        }
    };
    static volatile CountDownLatch cd = new CountDownLatch(10000);//倒計時
    public static class ParseDate implements Runnable {
        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                if (t1.get() == null) {
                    t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
                        @Override
                        protected void finalize() throws Throwable {
                            System.out.println(this.toString() + " is gc");
                        }
                    });
                    System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
                }
                Date t = t1.get().parse("2016-12-19 19:29:" + i % 60);
            } catch (ParseException e) {
                e.printStackTrace();
            } finally {
                cd.countDown();//完成 計數器減1
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            es.execute(new ParseDate(i));
        }
        cd.await();//等待全部線程 完成準備
        System.out.println("mission complete!!");
        t1 = null;
        System.gc();
        System.out.println("first GC complete!!");
        t1 = new ThreadLocal<>();
        cd = new CountDownLatch(1000);
        for (int i = 0; i < 10000; i++) {
            es.execute(new ParseDate(i));
        }
        cd.await();
        Thread.sleep(1000);
        System.gc();
        System.out.println("second GC complete!!");
    }
}

執行上面代碼的代碼,看結果,首先,線程池中10個線程都各自建立了一個SimpleDateFormat對象實例。接着進行第一次GC,能夠看到ThreadLocal對象被回收了。接着提交了第二次任務,此次同樣也建立了10個SimpleDateFormat對象。而後,進行第二次GC。能夠看到,在第二次GC後,第一次建立的10個SimpleDateFormat子類實例所有被回收。能夠看到咱們沒有手動remove(),但系統依然回收了它們。

對性能有何幫助

對於性能而言,是爲每一個線程分配一個獨立的對象取決於共享對象的內部邏輯。若是共享對象對於競爭的處理容易引發性能損失,咱們仍是應該考慮使用ThreadLocal爲每一個線程分配單獨的對象。一個典型的案例就是在多線程下產生隨機數。

public class RandomDemo {
    public static final int GET_COUNT = 10000000;
    public static final int THREAD_COUNT = 4;
    static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
    public static Random rnd = new Random(123);     //1 定義一個全局的Random,線程共用
    public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {    //2 每一個線程各使用一個random對象
        @Override
        protected Random initialValue() {
            return new Random(123);
        }
    };
    public static class RndTask implements Callable<Long> {
        private int mode = 0;

        public RndTask(int mode) {
            this.mode = mode;
        }

        public Random getRondom() {
            if (mode == 0) {
                return rnd;
            } else if (mode == 1) {
                return tRnd.get();
            } else {
                return null;
            }
        }

        /**
         * Computes a result, or throws an exception if unable to do so.
         *
         * @return computed result
         * @throws Exception if unable to compute a result
         */
        @Override
        public Long call() throws Exception {
            long b = System.currentTimeMillis();
            for (long i = 0; i < GET_COUNT; i++) {
                getRondom().nextInt();
            }
            long e = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");

            return e - b;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Future<Long>[] futs = new Future[THREAD_COUNT];     //3
        for (int i = 0; i < THREAD_COUNT; i++) {
            futs[i] = exe.submit(new RndTask(0));
        }
        long totaltime = 0;
        for (int i = 0; i < THREAD_COUNT; i++) {
            totaltime += futs[i].get();
        }
        System.out.println("多線程訪問同一個Random實例:" + totaltime + "ms");     //4

        for (int i = 0; i < THREAD_COUNT; i++) {
            futs[i] = exe.submit(new RndTask(1));
        }
        totaltime = 0;
        for (int i = 0; i < THREAD_COUNT; i++) {
            totaltime += futs[i].get();
        }
        System.out.println("使用ThreadLocal包裝Random實例:" + totaltime + "ms");
        exe.shutdown();
    }
}

在程序的1處定義了一個所有線程共用的Random、2處定義了 每一個線程獨自佔用的Random。mode用來切換是否使用獨佔的Random(0爲否,1爲是)。3~4處,打印出使用同一Random花費的時間,4處後面的代碼打印每一個線程獨佔(ThreadLocal包裝的)花費的時間。

無鎖

併發控制分爲兩種策略:樂觀策略和悲觀策略。鎖是一種悲觀策略、無鎖則是樂觀的,無鎖的策略使用一種比較交換的技術(CAS)來鑑別線程衝突。

無鎖的兩個好處:比有鎖的程序擁有更好的性能;天生就是對死鎖免疫的。

不同凡響的併發策略:計較交換(CAS)

CAS的算法過程:它包含三個參數:V(要更新的變量)、E(預期值)、N(新值)。僅當V值等於E值時,纔會將V值設爲N,若是V值和E值不一樣,則說明已經有其餘線程作了更新,則當前線程什麼都不作。最後,CAS返回當前V的真實值。失敗的線程不會被掛起,僅是被告知失敗,而且能夠再次嘗試。

在硬件層面,大部分的現代處理器都已經支持原子化的CAS指令。在JDK5.0後,這種操做無處不在。

無鎖的線程安全整數:AtomicInteger

與Integer不一樣的是,AtomicInteger是可變的,而且是線程安全的。對其進行修改等的任何操做,都是CAS進行的。

AtomicInteger的主要方法有:

AtomicInteger_2019-09-16_15-53-13

內部重要的兩個值

AtomicInteger的當前實際值:value

private volatile int value;

保存AtomicInteger對象中的偏移量:valueOffset,它是AtomicInteger的關鍵。

private static final long valueOffset;

簡單實例

public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger();

    public static class AddThread implements Runnable {
        public void run() {
            for (int k = 0; k < 10000; k++)
                i.incrementAndGet();    //1
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        for (int k = 0; k < 10; k++) 
            ts[k] = new Thread(new AddThread());  
        for (int k = 0; k < 10; k++) 
            ts[k].start();
        for (int k = 0; k < 10; k++) 
            ts[k].join();
        System.out.println(i);
    }
}

在1處,i.incrementAndGet()方法會使用CAS操做將本身+1,並返回當前值(這裏忽略了當前值)。執行完這段代碼,會輸出100000,這說明程序正常執行。

內部實現

基於JDK1.7的incrementAndGet(),JDK1.8有點不一樣。

public final int incrementAndGet(){
  for(;;){      //1
    int current = get();    //2
    int next = current + 1;
    if(compareAndSet(current, next))    //3
        return next;
  }
}

在1處中,爲何連設置一個值都須要一個死循環呢?緣由就是:CAS的操做未必是成功的,若不成功,咱們就要不斷的進行嘗試。在2處,用get()取得當前值,接着+1後獲得新值。在3處,使用compareAndSet將新值next寫入,成功的條件是,當前值應該等於剛剛取得的current。若是不是這樣,說明在2處到3處之間,有其餘線程已經修改過了,這種狀況被看作是過時狀態。所以,須要進行下一次嘗試,直到成功。

get():只是返回內部數據value

public final int get(){
  return value;
}

相似的類

和AtomicInteger相似的類還有AtomicLong、AtomicBoolean、AtomicReference(表示對象引用)

java中的指針:Unsafe類

java雖然拋棄了指針,可是在關鍵時刻,相似指針的技術仍是必不可少的。下面例子中的的Unsafe實現就是最好的例子。可是,開發人員並不但願你們使用這個類,下面只是瞭解一下這個類。

實例講解Unsafe

咱們對以前的compareAndSet()的實現進一步研究:

public final boolean compareAndSet(int expect, int update){
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

sum.misc.unsafe:上面的unsafe類,封裝了一些不安全的操做。java中去除了指針,而java中的Unsafe就封裝了一些相似指針的操做。

CompareAndSwapInt()方法是一個navtive方法。它的幾個參數以下:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

第一個參數o爲給定的對象,offset爲對象內的偏移量(其實就是一個字段到對象頭部的偏移量,一般這個偏移量能夠快速定位字段),expected表示指望值,x表示要設置的值。

3.3.4中的ConcurrentLinkedQueue中的Node的一些CAS操做也都是使用Unsafe來實現的。

無鎖的對象引用:AtomicReference

AtomicReference和AtomicInteger很是相似,不一樣之處:AtomicInteger是對整數的封裝,而AtomicReference是對普通的對象引用。

CAS一個小小的例外

線程判斷被修改對象是否被正確寫入的條件是當前值是否等於指望值。這個邏輯通常是正確的,可是有一個例外:

CAS_2019-09-16_16-24-57

圖中顯示,當得到對象數據後,在準備修改成新值前。線程B將對象的值修改了兩次,而兩次修改後,對象的值又恢復爲舊值。如果簡單的數值加法,這沒什麼,即便這個數字再怎麼修改,只要他最終改回指望值,加法計算就不會錯。

可是,在現實中,還存在另外一種常見,就是咱們是否能修改對象的值,不只取決於當前值,還取決於對象的變化過程。

好比:有一家蛋糕店,決定爲貴賓卡里餘額小於20元的客戶一次性贈送20元,刺激消費者充值和消費。但條件是,每一位用戶只能被贈送一次。

stotic AtomicReference<Integer> money = new AtomicReference<Integer>();

接着,啓動若干個線程,不斷掃描數據,併爲知足條件的客戶充值。

for(int i = 0; i<3; i++){
  new Thread(){
    public void run(){
      while(true){
        while(true){
          Integer m = money.get();
          if(m < 20){
            if(money.compareAndSet(m, m+20)){   //1
              System.out.println("餘額小於20,充值成功!");
              break;
            }
          }else{
            //餘額大於20,無需充值
            break;
          }
        }
      }
    }
  }
}

在1處,判斷用戶餘額並贈送金額。若此時被其餘線程處理,那麼當前線程就會失敗。這樣能夠保證用戶只會充值一次。

問題來了,此時,若很不幸,用戶正在消費,就在金額剛剛贈送的同時,他進行了一次消費,使總金額又小於20。使得消費、贈於後的的金額等於消費前,贈予前的金額。這時,後臺的贈予進程就會誤覺得這個帳戶尚未贈予。因此,存在被屢次贈予的可能。雖然概率很小。

解決辦法

使用AtomicStampedReference能夠很好的解決這個問題。

帶有時間軸的對象引用:AtomicStampedReference

AtomicReference沒法解決上述問題的根本緣由是對象在修改過程當中,丟失了狀態信息。

AtomicStampedReference不只維護了對象值,還維護了一個時間戳(實際上可使用任意整數表示狀態值)。當AtomicStampedReference對應的數值被修改時,除了更新數據自己外,還必需要更新時間軸。當AtomicStampedReference設置對象值時,對象值以及時間軸都必須知足指望值,寫入纔會成功。這樣,即便對象被重複讀寫,寫回原值,有了時間戳,也能防止不恰當的寫入。

經常使用api

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
//得到當前對象引用
public V getReference()
//得到當前時間戳
public int getStamp()
//設置當前對象引用和時間戳
public void set(V newReference, int newStamp)

例子

public class AtomicStampedReferenceDemo {
    static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);

    public static void main(String[] args) {
        // 模擬多個線程同時更新後臺數據庫,爲用戶充值
        for (int i = 0; i < 3; i++) {
            final int timestamp = money.getStamp();
            new Thread() {
                public void run() {
                    while (true) {
                        while (true) {
                            Integer m = money.getReference();
                            if (m < 20) {
                                if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {     //1
                                    System.out.println("餘額小於20元,充值成功,餘額:"
                                            + money.getReference() + "元");
                                    break;
                                }
                            } else {
                                // System.out.println("餘額大於20元,無需充值");
                                break;
                            }
                        }
                    }
                }
            }.start();
        }

        // 用戶消費線程,模擬消費行爲
        new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    while (true) {
                        int timestamp = money.getStamp();
                        Integer m = money.getReference();
                        if (m > 10) {
                            System.out.println("大於10元");
                            if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {
                                System.out.println("成功消費10元,餘額:" + money.getReference());
                                break;
                            }
                        } else {
                            System.out.println("沒有足夠的金額");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                    }
                }
            }
        }.start();
    }
}

在1處,若是贈予成功,咱們修改時間戳,使得系統不可能發生二次贈予的狀況。消費線程也是,每次操做,都使得時間戳加1,使之不可能重複。

數組也能無鎖:AtomicIntegerArray

除了提供基本數據類型外,JDK也封裝了數組:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分別表示整數數組、Long型數組和普通對象數組。

以AtomicIntegerArray爲例。

AtomicIntegerArray本質上是對int[]類型的封裝。使用unsafe類經過CAS的方法控制int[]在多線程下的安全性。

幾個核心API

//得到數組第i個下標的元素
public final int get(int i)
//得到數組的長度
public final int length()
//將數組第i個小標設置爲newValue,並返回舊的值
public final int getAndSet(int i, int newValue)
//進行CAS操做,若是第i個下標的元素等於expect,則設置爲update,設置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//將第i個下標的元素加1
public final int  getAndIncrement(int i)
//將第i個下標的元素減1
public final int getAndDecrement(int i)
//將第i個下標元素加delta(delta能夠是負數)
public final int getAndAdd(int i, int delta)

簡單示例

public class AtomicIntegerArrayDemo {
   static AtomicIntegerArray arr = new AtomicIntegerArray(10);  //1
    public static class AddThread implements Runnable{
        public void run(){
           for(int k=0;k<1000;k++)
              arr.getAndIncrement(k%arr.length());
        }
    }
   public static void main(String[] args) throws InterruptedException {
        Thread[] ts=new Thread[10];
        for(int k=0;k<10;k++){
            ts[k]=new Thread(new AddThread());
        }
        for(int k=0;k<10;k++){ts[k].start();}
        for(int k=0;k<10;k++){ts[k].join();}
        System.out.println(arr);
   }
}

上述代碼中,1處申明瞭一個內含10個元素的數組。2處對數組內10個元素進行累加,每一個元素各加100。開啓10個線程數組中10個元素必然是1000。若線程不安全數組中10個元素可能部分或所有小於1000。

普通變量也可享受原子操做:AtomicIntegerFieldUpdate

有時候,因爲初期考慮不周,或後期需求變化,一些普通變量也會有線程安全的需求。這時候怎麼辦呢?咱們固然能夠簡單修改這個變量成線程安全的變量,但違反了開閉原則。這時候就要想到AtomicIntegerFieldUpdate了。

做用:它能夠在不改動(或極少改動)原有代碼的基礎上,讓普通的變量也可享受CAS操做帶來的線程安全性,來得到線程安全的保障。

有三種:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。分別能夠對int、long和普通對象進行CAS修改。

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate{
        int id;
        volatile int score;
    }
    public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater
            = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");   //1
    //檢查Updater是否工做正確
    public static AtomicInteger allScore=new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        final Candidate stu=new Candidate();      //新建一個候選人
        Thread[] t=new Thread[10000];
        for(int i = 0 ; i < 10000 ; i++) {
            t[i]=new Thread() {
                public void run() {
                    if(Math.random()>0.4){
                        scoreUpdater.incrementAndGet(stu);  //2
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }
        for(int i = 0 ; i < 10000 ; i++) {  t[i].join();}
        System.out.println("score="+stu.score);
        System.out.println("allScore="+allScore);
    }
}

代碼1處定義了一個AtomicIntegerFieldUpdater實例,用來對Candidate.score進行寫入。用allScore來校驗 scoreUpdater 的正確性。代碼2處對stu的score進行增1操做。

AtomicIntegerFieldUpdater的幾個注意事項:

  • Updater只能修改它可見範圍內的變量。由於Updater使用反射獲得這個變量。若是變量不可見,就會出錯。如score設置爲private,就是不可見的。
  • 爲了確保變量被正確讀取,它必須時volatile類型的。
  • 因爲CAS操做會經過對象實例中的偏移量直接賦值,所以,它不支持靜態變量。

讓線程之間互相幫助:SynchronousQueue的實現

死鎖

在大部分應用程序中,使用鎖的狀況通常要多於無鎖。

死鎖:死鎖就是兩個或者多個線程,相互佔用對方須要的資源,而都不進行釋放,致使彼此之間都相互等待對方釋放資源,產生無限等待的現象。

對系統的影響:死鎖一旦出現,若是沒有外力介入,將永遠存在,這對系統產生嚴重影響。

簡單的例子:

public class DeadLock extends Thread {
   protected Object tool;
   static Object fork1 = new Object();
   static Object fork2 = new Object();
   public DeadLock(Object obj) {
      this.tool = obj;
      if(tool == fork1) {
         this.setName("哲學家A");
      }
      if(tool == fork2) {
         this.setName("哲學家B");
      }
   }
   @Override
   public void run() {
      if(tool == fork1) {
         synchronized (fork1) {
            try {
               Thread.sleep(500);
            } catch (Exception e) {
               e.printStackTrace();
            }
            synchronized (fork2) {
               System.out.println("哲學家A開始吃飯了");
            }
         }
      }
      if(tool == fork2) {
         synchronized (fork2) {
            try {
               Thread.sleep(500);
            } catch (Exception e) {
               e.printStackTrace();
            }
            synchronized (fork1) {
               System.out.println("哲學家B開始吃飯了");
            }
         }
      }
   }
   public static void main(String[] args) throws InterruptedException {
      DeadLock A = new DeadLock(fork1);
      DeadLock B = new DeadLock(fork2);
      A.start();
      B.start();
      Thread.sleep(1000);
   }
}

上面例子中,兩個哲學家吃飯都須要兩把叉子,當只有兩把叉子時,而兩個哲學家又一人槍了一把叉子,在等對方手裏的第而把叉子吃飯,這時,兩個哲學家都互相佔用對方的叉子,因而,兩個哲學家將一直等待下去,從而產生了死鎖。

相關文章
相關標籤/搜索