<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          寫(xiě)時(shí)復(fù)制(Copy-On-Write)在Java中是如何被應(yīng)用的嗎?

          共 11837字,需瀏覽 24分鐘

           ·

          2021-08-11 13:09

          前言

          寫(xiě)時(shí)復(fù)制(Copy-on-write,簡(jiǎn)稱COW)是一種計(jì)算機(jī)程序設(shè)計(jì)領(lǐng)域的優(yōu)化策略。

          其核心思想是,如果有多個(gè)調(diào)用者同時(shí)請(qǐng)求相同資源(如內(nèi)存或磁盤(pán)上的數(shù)據(jù)存儲(chǔ)),他們會(huì)共同獲取相同的指針指向相同的資源,直到某個(gè)調(diào)用者試圖修改資源的內(nèi)容時(shí),系統(tǒng)才會(huì)真正復(fù)制一份專用副本(private copy)給該調(diào)用者,而其他調(diào)用者所見(jiàn)到的最初的資源仍然保持不變。這個(gè)過(guò)程對(duì)其他的調(diào)用者是透明的(transparently)。

          此作法的主要優(yōu)點(diǎn)是如果調(diào)用者沒(méi)有修改該資源,就不會(huì)有副本(private copy)被建立,因此多個(gè)調(diào)用者只是讀取操作時(shí)可以共享同一份資源。

          COW(奶牛)技術(shù)的應(yīng)用場(chǎng)景很多,Linux通過(guò)Copy On Write技術(shù)極大地減少了Fork的開(kāi)銷(xiāo)。文件系統(tǒng)通過(guò)Copy On Write技術(shù)一定程度上保證數(shù)據(jù)的完整性。數(shù)據(jù)庫(kù)服務(wù)器也一般采用了寫(xiě)時(shí)復(fù)制策略,為用戶提供一份snapshot。

          而JDK的CopyOnWriteArrayList/CopyOnWriteArraySet容器也采用了 COW思想,它是如何工作的是本文討論的重點(diǎn)。

          Vector和synchronizedList

          我們知道ArrayList是線程不安全的,而Vector是線程安全的容器。查看源碼可以知道,Vector之所以線程安全,是因?yàn)樗鼛缀踉诿總€(gè)方法聲明處都加了synchronized關(guān)鍵字來(lái)使整體方法原子化。

          另外,使用Collections.synchronizedList(new ArrayList())修飾后,新建出來(lái)的ArrayList也是安全的,它是如何實(shí)現(xiàn)的呢?查看源碼發(fā)現(xiàn),它也是幾乎在每個(gè)方法都加上synchronized關(guān)鍵字使方法原子化,只不過(guò)它不是把synchronized加在方法的聲明處,而是加在方法的內(nèi)部。

          容器是線程安全的,并不意味著就可以在多線程環(huán)境下放心大膽地隨便用了,來(lái)看下面這段使用Vector的代碼。

          @Test
          public void testVectorConcurrentReadWrite() {
              Vector<Integer> vector = new Vector<>();
              vector.add(1);
              vector.add(2);
              vector.add(3);
              vector.add(4);
              vector.add(5);
              for (Integer item : vector) {
                  new Thread(vector::clear).start();
                  System.out.println(item);
              }
          }

          運(yùn)行結(jié)果如下:

          在一個(gè)線程中使用Iterator迭代器遍歷vector,同時(shí)另一個(gè)線程對(duì)vector作修改時(shí),會(huì)拋出java.util.ConcurrentModificationException異常。很多人不理解,因?yàn)閂ector的所有方法都加了synchronized關(guān)鍵字來(lái)修飾,包括迭代器方法,理論上應(yīng)該是線程安全的呀。

          public synchronized Iterator<E> iterator() {
           //Itr是AbstractList的私有內(nèi)部類
              return new Itr();
          }

          看以上錯(cuò)誤的堆棧指向java.util.Vector$Itr.checkForComodification(Vector.java:1184),源碼如下:

          兩個(gè)關(guān)鍵變量:

          • expectedModCount:表示對(duì)List修改次數(shù)的期望值,它的初始值與modCount相等
          • modCount:表示List集合結(jié)構(gòu)被修改次數(shù),是AbstractList類中的一個(gè)成員變量,初始值為0

          看過(guò)ArrayList的源碼就知道,每次調(diào)用add()和remove()方法時(shí)就會(huì)對(duì)modCount進(jìn)行加1操作。而我們上面的測(cè)試代碼中調(diào)用了Vector類的clear()方法,這個(gè)方法中對(duì)modCount進(jìn)行了加1,而迭代器中的expectedModCount依然等于0,兩者不等,因此拋了異常。這就是集合中的fail-fast機(jī)制,fail-fast 機(jī)制用來(lái)防止在對(duì)集合進(jìn)行遍歷過(guò)程當(dāng)中,出現(xiàn)意料之外的修改,會(huì)通過(guò)Unchecked異常暴力的反應(yīng)出來(lái)。

          雖然Vector的方法都采用了synchronized進(jìn)行了同步,但是實(shí)際上通過(guò)Iterator訪問(wèn)的情況下,每個(gè)線程里面返回的是不同的iterator,也即是說(shuō)expectedModCount變量是每個(gè)線程私有。如果此時(shí)有2個(gè)線程,線程1在進(jìn)行遍歷,線程2在進(jìn)行修改,那么很有可能導(dǎo)致線程2修改后導(dǎo)致Vector中的modCount自增了,線程2的expectedModCount也自增了,但是線程1的expectedModCount沒(méi)有自增,此時(shí)線程1遍歷時(shí)就會(huì)出現(xiàn)expectedModCount不等于modCount的情況了。

          同樣地,SynchronizedList在使用迭代器遍歷的時(shí)候同樣會(huì)有問(wèn)題的,源碼中的注釋已經(jīng)提醒我們要手動(dòng)加鎖了。

          foreach循環(huán)里不能調(diào)用集合的remove/add/clear方法這一條規(guī)約不僅對(duì)非線程安全的ArrayList/LinkedList適用,對(duì)于線程安全的Vector以及synchronizedList也同樣適用。

          因此,要想解決以上問(wèn)題,只能在遍歷前(無(wú)論用不用iterator)加鎖。

          synchronized (vector) {
              for (int i = 0; i < vector.size(); i++) {
                  System.out.println(vector.get(i));
          }
          //或者
          synchronized (vector) {
              for (Integer item : vector) {
                  System.out.println(item);
          }

          僅僅是遍歷一下容器都要上鎖,性能必然不好。

          其實(shí)并非只有遍歷前加鎖這一種解決方法,使用并發(fā)容器CopyOnWriteArrayList也能避免以上問(wèn)題。往期面試題匯總:250期面試資料

          CopyOnWriteArrayList介紹

          一般來(lái)說(shuō),我們會(huì)認(rèn)為:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。

          無(wú)論是Hashtable–>ConcurrentHashMap,還是說(shuō)Vector–>CopyOnWriteArrayList。JUC下支持并發(fā)的容器與老一代的線程安全類相比,總結(jié)起來(lái)就是加鎖粒度的問(wèn)題。

          • Hashtable與Vector加鎖的粒度大,直接在方法聲明處使用synchronized
          • ConcurrentHashMap、CopyOnWriteArrayList的加鎖粒度小。用各種方式來(lái)實(shí)現(xiàn)線程安全,比如我們知道的ConcurrentHashMap用了CAS、+ volatile等方式來(lái)實(shí)現(xiàn)線程安全
          • JUC下的線程安全容器在遍歷的時(shí)候不會(huì)拋出ConcurrentModificationException異常

          所以一般來(lái)說(shuō),我們都會(huì)使用JUC包下給我們提供的線程安全容器,而不是使用老一代的線程安全容器。

          下面我們來(lái)看看CopyOnWriteArrayList是怎么實(shí)現(xiàn)的,為什么使用迭代器遍歷的時(shí)候就不用額外加鎖,也不會(huì)拋出ConcurrentModificationException異常。

          實(shí)現(xiàn)原理

          Copy-on-write是解決并發(fā)的的一種思路,指的是實(shí)行讀寫(xiě)分離,如果執(zhí)行的是寫(xiě)操作,則復(fù)制一個(gè)新集合,在新集合內(nèi)添加或者刪除元素。待一切修改完成之后,再將原集合的引用指向新的集合。

          這樣的好處就是,可以高并發(fā)地對(duì)COW進(jìn)行讀和遍歷操作,而不需要加鎖,因?yàn)楫?dāng)前集合不會(huì)添加任何元素。

          寫(xiě)時(shí)復(fù)制(copy-on-write)的這種思想,這種機(jī)制,并不是始于Java集合之中,在Linux、Redis、文件系統(tǒng)中都有相應(yīng)思想的設(shè)計(jì),是一種計(jì)算機(jī)程序設(shè)計(jì)領(lǐng)域的優(yōu)化策略。

          CopyOnWriteArrayList的核心理念就是讀寫(xiě)分離,寫(xiě)操作在一個(gè)復(fù)制的數(shù)組上進(jìn)行,讀操作還是在原始數(shù)組上進(jìn)行,讀寫(xiě)分離,互不影響。寫(xiě)操作需要加鎖,防止并發(fā)寫(xiě)入時(shí)導(dǎo)致數(shù)據(jù)丟失。寫(xiě)操作結(jié)束之后需要讓數(shù)組指針指向新的復(fù)制數(shù)組。

          看一下CopyOnWriteArrayList基本的結(jié)構(gòu)。

          看一下其讀寫(xiě)的源碼,寫(xiě)操作加鎖,防止并發(fā)寫(xiě)入時(shí)導(dǎo)致數(shù)據(jù)丟失,并復(fù)制一個(gè)新數(shù)組,增加操作在新數(shù)組上完成,將array指向到新數(shù)組中,最后解鎖。至于讀操作,則是直接讀取array數(shù)組中的元素。

          public boolean add(E e) {
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  Object[] elements = getArray();
                  int len = elements.length;
                  Object[] newElements = Arrays.copyOf(elements, len + 1);
                  newElements[len] = e;
                  setArray(newElements);
                  return true;
              } finally {
                  lock.unlock();
              }
          }

          final void setArray(Object[] a) {
              array = a;
          }

          public E get(int index) {
              return get(getArray(), index);
          }

          final Object[] getArray() {
              return array;
          }

          遍歷 - COWIterator

          到現(xiàn)在,還是沒(méi)有解釋為什么CopyOnWriteArrayList在遍歷時(shí),對(duì)其進(jìn)行修改而不拋出異常。

          不管是foreach循環(huán)還是直接寫(xiě)Iterator來(lái)遍歷,實(shí)際上都是使用Iterator遍歷。那么就直接來(lái)看下CopyOnWriteArrayList的iterator()方法。

          public Iterator<E> iterator() {
              return new COWIterator<E>(getArray(), 0);
          }

          來(lái)看一下迭代器COWIterator的實(shí)現(xiàn)源碼。

          可以發(fā)現(xiàn)的是,迭代器所有的操作都基于snapshot數(shù)組,而snapshot是傳遞進(jìn)來(lái)的array數(shù)組。

          也就是說(shuō)在使用COWIterator進(jìn)行遍歷的時(shí)候,如果修改了集合,集合內(nèi)部的array就指向了新的一個(gè)數(shù)組對(duì)象,而COWIterator內(nèi)部的那個(gè)snapshot還是指向初始化時(shí)傳進(jìn)來(lái)的舊數(shù)組,所以不會(huì)拋異常,因?yàn)榕f數(shù)組永遠(yuǎn)沒(méi)變過(guò),舊數(shù)組讀操作永遠(yuǎn)可靠且安全。往期面試題匯總:250期面試資料

          CopyOnWriteArrayList與synchronizedList性能測(cè)試

          寫(xiě)單元測(cè)試來(lái)對(duì)CopyOnWriteArrayList與synchronizedList的并發(fā)寫(xiě)性能作測(cè)試,由于CopyOnWriteArrayList寫(xiě)時(shí)直接復(fù)制新數(shù)組,可以預(yù)想到其寫(xiě)操作性能不高,會(huì)劣于synchronizedList。

          @Test
          public void testThreadSafeListWrite() {
              List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
              List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
              StopWatch stopWatch = new StopWatch();
              int loopCount = 10000;
              stopWatch.start();
              /**
               * ThreadLocalRandom:是JDK 7之后提供并發(fā)產(chǎn)生隨機(jī)數(shù),能夠解決多個(gè)線程發(fā)生的競(jìng)爭(zhēng)爭(zhēng)奪。
               * ThreadLocalRandom不是直接用new實(shí)例化,而是第一次使用其靜態(tài)方法current()。
               * 從Math.random()改變到ThreadLocalRandom有如下好處:我們不再有從多個(gè)線程訪問(wèn)同一個(gè)隨機(jī)數(shù)生成器實(shí)例的爭(zhēng)奪。
               */

              IntStream.rangeClosed(1, loopCount).parallel().forEach(
                      item -> copyOnWriteArrayList.add(ThreadLocalRandom.current().nextInt(loopCount)));
              stopWatch.stop();
              System.out.println(
                      "Write:copyOnWriteList: " + stopWatch.getTime() + ",copyOnWriteList.size()=" + copyOnWriteArrayList
                              .size());

              stopWatch.reset();
              stopWatch.start();
              /**
               * parallelStream特點(diǎn):基于服務(wù)器內(nèi)核的限制,如果你是八核
               * 每次線程只能起八個(gè),不能自定義線程池
               */

              IntStream.rangeClosed(1, loopCount).parallel().forEach(
                      item -> synchronizedList.add(ThreadLocalRandom.current().nextInt(loopCount)));
              stopWatch.stop();
              System.out.println(
                      "Write:synchronizedList: " + stopWatch.getTime() + ",synchronizedList.size()=" + synchronizedList
                              .size());
          }

          運(yùn)行結(jié)果如下,可以看到同樣條件下的寫(xiě)耗時(shí),CopyOnWriteArrayList是synchronizedList的30多倍。

          同樣地,寫(xiě)單元測(cè)試來(lái)對(duì)CopyOnWriteArrayList與synchronizedList的并發(fā)讀性能作測(cè)試,由于CopyOnWriteArrayList讀操作不加鎖,可以預(yù)想到其讀操作性能明顯會(huì)優(yōu)于synchronizedList。

          @Test
          public void testThreadSafeListRead() {
              List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
              List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
              copyOnWriteArrayList.addAll(IntStream.rangeClosed(11000000).boxed().collect(Collectors.toList()));
              synchronizedList.addAll(IntStream.rangeClosed(11000000).boxed().collect(Collectors.toList()));

              int copyOnWriteArrayListSize = copyOnWriteArrayList.size();
              StopWatch stopWatch = new StopWatch();
              int loopCount = 1000000;
              stopWatch.start();
              /**
               * ThreadLocalRandom:是JDK 7之后提供并發(fā)產(chǎn)生隨機(jī)數(shù),能夠解決多個(gè)線程發(fā)生的競(jìng)爭(zhēng)爭(zhēng)奪。
               * ThreadLocalRandom不是直接用new實(shí)例化,而是第一次使用其靜態(tài)方法current()。
               * 從Math.random()改變到ThreadLocalRandom有如下好處:我們不再有從多個(gè)線程訪問(wèn)同一個(gè)隨機(jī)數(shù)生成器實(shí)例的爭(zhēng)奪。
               */

              IntStream.rangeClosed(1, loopCount).parallel().forEach(
                      item -> copyOnWriteArrayList.get(ThreadLocalRandom.current().nextInt(copyOnWriteArrayListSize)));
              stopWatch.stop();
              System.out.println("Read:copyOnWriteList: " + stopWatch.getTime());

              stopWatch.reset();
              stopWatch.start();
              int synchronizedListSize = synchronizedList.size();
              /**
               * parallelStream特點(diǎn):基于服務(wù)器內(nèi)核的限制,如果你是八核
               * 每次線程只能起八個(gè),不能自定義線程池
               */

              IntStream.rangeClosed(1, loopCount).parallel().forEach(
                      item -> synchronizedList.get(ThreadLocalRandom.current().nextInt(synchronizedListSize)));
              stopWatch.stop();
              System.out.println("Read:synchronizedList: " + stopWatch.getTime());
          }

          運(yùn)行結(jié)果如下,同等條件下的讀耗時(shí),CopyOnWriteArrayList只有synchronizedList的一半。

          CopyOnWriteArrayList優(yōu)缺點(diǎn)總結(jié)

          優(yōu)點(diǎn):

          • 對(duì)于一些讀多寫(xiě)少的數(shù)據(jù),寫(xiě)入時(shí)復(fù)制的做法就很不錯(cuò),例如配置、黑名單、物流地址等變化非常少的數(shù)據(jù),這是一種無(wú)鎖的實(shí)現(xiàn)。可以幫我們實(shí)現(xiàn)程序更高的并發(fā)。
          • CopyOnWriteArrayList并發(fā)安全且性能比Vector好。Vector是增刪改查方法都加了synchronized 來(lái)保證同步,但是每個(gè)方法執(zhí)行的時(shí)候都要去獲得鎖,性能就會(huì)大大下降,而CopyOnWriteArrayList只是在增刪改上加鎖,但是讀不加鎖,在讀方面的性能就好于Vector。

          缺點(diǎn):

          • 數(shù)據(jù)一致性問(wèn)題。CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。比如線程A在迭代CopyOnWriteArrayList容器的數(shù)據(jù)。線程B在線程A迭代的間隙中將CopyOnWriteArrayList部分的數(shù)據(jù)修改了,但是線程A迭代出來(lái)的是舊數(shù)據(jù)。
          • 內(nèi)存占用問(wèn)題。如果CopyOnWriteArrayList經(jīng)常要增刪改里面的數(shù)據(jù),并且對(duì)象比較大,頻繁地寫(xiě)會(huì)消耗內(nèi)存,從而引發(fā)Java的GC問(wèn)題,這個(gè)時(shí)候,我們應(yīng)該考慮其他的容器,例如ConcurrentHashMap。

          感謝閱讀,希望對(duì)你有所幫助 :) 

          來(lái)源:blog.csdn.net/fuzhongmin05/article/details/117076906


          最近給大家找了  Vue進(jìn)階


          資源,怎么領(lǐng)?。?/span>


          掃二維碼,加我微信,回復(fù):Vue進(jìn)階

           注意,不要亂回復(fù) 

          沒(méi)錯(cuò),不是機(jī)器人
          記得一定要等待,等待才有好東西


          瀏覽 26
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91成人版| 欧美mv日韩mv国产网站app | av网站免费观看 gogo高清无码 | 草久影视| 2019天天操 |