偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Java 并發(fā)編程對(duì)象組合與封閉性實(shí)踐指南

開(kāi)發(fā) 前端
本文通過(guò)對(duì)象組合取代繼承、可監(jiān)視鎖、final不可變安全發(fā)布等核心技術(shù)演示了并發(fā)編程中一些開(kāi)發(fā)技巧,希望對(duì)你有幫助。

本文將介紹一種基于對(duì)象組合哲學(xué)的并發(fā)編程的封裝技術(shù),確保團(tuán)隊(duì)在開(kāi)發(fā)過(guò)程中,即使對(duì)整體項(xiàng)目不是非常了解的情況下,依然可以明確一個(gè)類的線程安全性。

一、對(duì)象組合與安全委托

1. 實(shí)例封閉技術(shù)

為了保證并發(fā)操作場(chǎng)景下實(shí)例訪問(wèn)的安全性,我們可利用組合的方式將實(shí)例委托給其它實(shí)例,即基于該委托類對(duì)外暴露實(shí)例的部分操作,封閉風(fēng)險(xiǎn)調(diào)用,確保對(duì)象訪問(wèn)時(shí)是安全且一致的。就像下圖這樣,將obj委托給delegate進(jìn)行管理,將set操作封閉不對(duì)外暴露,確保僅通過(guò)暴露只讀避免對(duì)象逸出:

對(duì)應(yīng)的,如果我們想實(shí)現(xiàn)一個(gè)線程安全的HashMap緩存的安全發(fā)布和訪問(wèn),對(duì)應(yīng)落地技巧為:

  • HashMap實(shí)例私有封閉
  • 基于final保證HashMap域的不可變
  • 采用同一粒度的類鎖發(fā)布HashMap的讀寫操作一致和安全,同時(shí)保證外部不可直接操作cache

如下所示,我們隱藏了HashMap部分操作,同時(shí)基于監(jiān)視鎖synchronized 保證讀寫操作可見(jiàn)且安全:

public class Cache {

    //實(shí)例私有并在內(nèi)部完成初始化
    private static final Map<String, Object> cache = new HashMap<>();

    
    public static synchronized void put(String key, Object object) {
        cache.put(key, object);
    }

    public static synchronized Object get(String key) {
        return cache.get(key);
    }
}

需要注意的時(shí),筆者上文強(qiáng)調(diào)的是被委托的容器cache的安全,基于get方法訪問(wèn)到object還是會(huì)被發(fā)布出去,此時(shí)就可能在并發(fā)操的線程安全問(wèn)題:

所以如果開(kāi)發(fā)人員需要保證讀取對(duì)象的安全,建議用存儲(chǔ)的值也采用final修飾一下后存入容器中。

public static void main(String[] args) {
        final User user = new User(4,"val-4");
        put("k-1", user);
    }
    
   private static class User{
        //使用final修飾保證對(duì)應(yīng)成員域不可修改
        private final int id;
        private final String name;


       public User(int id, String name) {
           this.id = id;
           this.name = name;
       }
   }

2. 基于監(jiān)視器模式的對(duì)象訪問(wèn)

從線程封閉原則及邏輯推論可以得出java監(jiān)視器模式,對(duì)于并發(fā)操作下的對(duì)象讀訪問(wèn),我們可以采用監(jiān)視器模式將可變狀態(tài)加以封裝,我們以常用的java list為例,整體封裝思路為:

  • 將需要管理的被委托的List以不可變的成員域的方式組合到SafeList 中
  • 使用final保證列表安全初始化且不可變
  • List選用不可變列表,做好安全兜底,避免順序等遭到破壞
  • 屏蔽所有容器的刪改操作
  • 訪問(wèn)對(duì)象在進(jìn)行必要性校驗(yàn)后,返回深拷貝的對(duì)象,不暴露容器內(nèi)部細(xì)節(jié)

對(duì)應(yīng)的代碼如下所示:

public class SafeList {

    //final修飾保證list安全初始化
    private final List<Person> list;


    public SafeList(List<Person> list) {
        //使用不可變方法為容器做好安全兜底,保證列表不可進(jìn)行增、閃、刪、改操作
        this.list = Collections.unmodifiableList(list);
    }


    //通過(guò)拷貝將對(duì)象安全發(fā)布出去,因?yàn)橹蛔x所以無(wú)需上鎖
    public Person getPerson(int idx) {
        if (idx >= list.size()) {
            throw new RuntimeException("index out of bound");
        }
        Person person = list.get(idx);
        return new Person(person.getId(),person.getName());
    }

}

對(duì)應(yīng)為了保證代碼的完整性我們也給出Person 類的實(shí)現(xiàn):

public class Person {
    private  int id;
    private  String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

   //get set ......
}

3. 對(duì)象不可變性簡(jiǎn)化委托

基于監(jiān)視器模式我們可以很好的保證對(duì)象的安全訪問(wèn),實(shí)際上我們可以做好更好,上文通過(guò)實(shí)例封閉和僅只讀權(quán)限保證容器的并發(fā)操作安全,同時(shí)在只讀操作返回Person 時(shí)我們也用了深拷貝發(fā)布一個(gè)全新的實(shí)例出去,保證容器內(nèi)部的元素不可變,實(shí)際上如果我們能夠?qū)erson 屬性保證不可變的情況下將其委托給容器,訪問(wèn)操作也可以直接返回:

public class Person {
    private final int id;
    private final String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
   
}

由此我們的代碼就可以簡(jiǎn)化成下面這樣,因?yàn)楸苊獾膶?duì)象拷貝的過(guò)程,程序性能也得到提升:

public Person getPerson(int idx) {
        if (idx >= list.size()) {
            throw new RuntimeException("index out of bound");
        }
        //person字段不可變,可直接返回
        return list.get(idx);
    }

對(duì)應(yīng)的我們基于下屬代碼針對(duì)Person拷貝發(fā)布和只讀封裝兩種模式進(jìn)行壓測(cè),對(duì)應(yīng)結(jié)果為:

  • 拷貝發(fā)布因?yàn)榭截惖拈_(kāi)銷耗時(shí)353ms
  • 采用只讀發(fā)布的耗時(shí)為152ms
//生成測(cè)試樣本
        List<Person> personList = IntStream.rangeClosed(1, 500_0000).parallel()
                .boxed()
                .map(i -> new Person(i, RandomUtil.randomString(10)))
                .collect(Collectors.toList());
        //生成安全容器
        SafeList safeList = new SafeList(personList);
        //進(jìn)行并發(fā)訪問(wèn)壓測(cè)
        int threadSize = 1000;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        ExecutorService threadPool = Executors.newFixedThreadPool(threadSize);

        long begin = System.currentTimeMillis();

        for (int i = 0; i < threadSize; i++) {
            threadPool.execute(() -> {
                Person person = safeList.getPerson(RandomUtil.randomInt(500_0000));
                boolean b = 1 != 1;
                if (b) {
                    Console.log(JSONUtil.toJsonStr(person));
                }
                countDownLatch.countDown();
            });
        }


        countDownLatch.await();
        long end = System.currentTimeMillis();
        //計(jì)算輸出耗時(shí)
        Console.log("cost:{}ms", end - begin);
        //關(guān)閉線程池
        threadPool.shutdownNow();

4. 原子維度的訪問(wèn)

如果我們被委托的對(duì)象是要求可變的,那么我們就需要保證所有字段的操作是互斥且原子的。例如我們現(xiàn)在要委托給容器一個(gè)坐標(biāo)對(duì)象,因?yàn)樽鴺?biāo)的值會(huì)實(shí)時(shí)改變的,所以在進(jìn)行坐標(biāo)操作時(shí),我們必須保證讀寫的一致性,即set和get都必須一次性針對(duì)x、y,從而避免當(dāng)為非原子操作讀取操一些異常的做坐標(biāo)。

將兩者分開(kāi)處理則可能會(huì)因?yàn)榉窃硬僮髟诓l(fā)情況下看到一個(gè)非法的邏輯坐標(biāo),例如:

  • 坐標(biāo)發(fā)生改變,線程0進(jìn)入修改,調(diào)用setX修改x坐標(biāo)。
  • 線程2訪問(wèn),看到一個(gè)修改后的x和未修改的y,定位異常。
  • 線程1修改y坐標(biāo)。

正確的坐標(biāo)設(shè)置方式如下代碼所示,即x、y保證同時(shí)進(jìn)行讀寫保證正確的坐標(biāo)更新與讀?。?/p>

public class SafePoint {
    private int x;
    private int y;

    public SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //原子維度操作保證操作的一致性
    public synchronized void setXandY(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //原子返回保證x、y,保證看到x、y實(shí)時(shí)一致修改后的值
    public synchronized int[] getXandY() {
        return new int[]{x, y};
    }
}

所以對(duì)于相關(guān)聯(lián)的字段,除了必要的同步鎖操作,我們還需要在將操作進(jìn)行原子化,保證讀取數(shù)據(jù)的實(shí)時(shí)正確一致。

二、現(xiàn)有容器的并發(fā)安全的封裝哲學(xué)

1. 使用繼承

Java類庫(kù)中內(nèi)置了許多見(jiàn)狀的基礎(chǔ)模塊類,日常使用時(shí)我們應(yīng)該優(yōu)先重要這些類,然后在此基礎(chǔ)上將類進(jìn)行拓展封裝,例如我們基于古典的線程安全列表vector實(shí)現(xiàn)一個(gè)若沒(méi)有對(duì)應(yīng)元素則添加的操作:

public class BetterVector extends Vector {
    
    //通過(guò)繼承獲取vector的api完成如果沒(méi)有則添加的線程安全原子操作
    public synchronized void addIfAbsent(Object o) {
        if (!contains(o)) {
            super.add(o);
        }
    }
}

當(dāng)然這種方法也是存在風(fēng)險(xiǎn)的:

  • 它暴露了vector的其他方法
  • 開(kāi)發(fā)者如果對(duì)于BetterVector沒(méi)有詳細(xì)的了解的話,可能還是會(huì)將contain和add操作錯(cuò)誤的組合使用,操作一致性問(wèn)題。

例如下圖所示步驟:

  • 線程0先判斷1不存在釋放鎖
  • 線程1判斷1不存在添加
  • 線程0基于contain操作結(jié)果即1不存在將元素1添加

此時(shí)vector就出現(xiàn)兩個(gè)1:

2. 使用組合

所以我們推薦實(shí)用組合的方式,通過(guò)將需要拓展的容器以組合的方式屏蔽內(nèi)置容器的實(shí)現(xiàn)細(xì)節(jié):

private List<Person> list = new ArrayList<>();
    
    public synchronized void addIfAbsent(Person person) {
        if (list.isEmpty()) {
            list.add(person);
        }
    }

但需要注意對(duì)于組合操作下操作粒度鎖的把控,例如下面這段代碼:

public class SafeList {
    private final List<Person> list;

    public SafeList(List<Person> list) {
        this.list = Collections.synchronizedList(list);
     
    }

    //當(dāng)前方法鎖的粒度是被委托的實(shí)例
    public synchronized void addIfAbsent(Person person) {
        if (list.isEmpty()) {
            list.add(person);
        }
    }

    public void add(Person person) {
        //add操作查看底層源碼用的鎖是 mutex = this;
        list.add(person);
    }
}

咋一看沒(méi)什么問(wèn)題,本質(zhì)上都是上了鎖,實(shí)際上add和addIfAbsent用的是兩把鎖:

  • addIfAbsent用的是當(dāng)前SafeList實(shí)例作為鎖
  • 而add因?yàn)橹苯訌?fù)用add方法所以用的是synchronizedList的對(duì)象鎖

這就使得addIfAbsent操作不是原子的,即在addIfAbsent操作期間,其他線程是可以直接調(diào)用list的api:

所以正確的做法是基于被組合安全容器的鎖,構(gòu)建相同維度的拓展方法:

private List<Person> list = Collections.synchronizedList(new ArrayList<>());
    //當(dāng)前方法鎖的粒度是被委托的實(shí)例
    public  void addIfAbsent(Person person) {
        synchronized (list) {
            if (list.isEmpty()) {
                list.add(person);
            }
        }
        
    }

    public  void add(Person person) {
        //add操作查看底層源碼用的鎖是 mutex = this;
        list.add(person);
    }
責(zé)任編輯:趙寧寧 來(lái)源: 寫代碼的SharkChili
相關(guān)推薦

2017-07-12 07:27:11

數(shù)據(jù)庫(kù)中間表存儲(chǔ)

2011-12-12 11:16:02

iOS并發(fā)編程

2024-05-21 09:55:43

AspectOrientedAOP

2010-11-17 11:31:22

Scala基礎(chǔ)面向?qū)ο?/a>Scala

2023-11-27 18:07:05

Go并發(fā)編程

2025-02-13 07:42:35

2024-04-02 11:34:09

成員對(duì)象封閉類C++

2023-12-11 15:32:30

面向?qū)ο缶幊?/a>OOPpython

2023-01-05 12:30:32

Redis

2023-07-06 08:06:47

LockCondition公平鎖

2023-09-26 10:30:57

Linux編程

2023-09-12 13:48:47

2024-02-26 08:33:51

并發(fā)編程活躍性安全性

2023-04-06 00:15:03

JavaReentrantL線程

2011-11-29 14:37:41

2021-07-03 17:44:34

并發(fā)高并發(fā)原子性

2017-09-19 14:53:37

Java并發(fā)編程并發(fā)代碼設(shè)計(jì)

2024-09-29 10:39:14

并發(fā)Python多線程

2022-03-09 23:02:30

Java編程處理模型

2009-07-08 16:10:24

Scala簡(jiǎn)介面向?qū)ο?/a>函數(shù)式
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)