무시하기 쉬운 ConcurrentHashMap 스레드 비보안 동작

9893 단어 스레드 보안
  • 라인 안전에 대한 기초 지식
  • 먼저 라인 안전이 무엇인지 설명한다. 다중 라인에서 하나의 데이터 유형의 매개 변수를 공유할 때 각 라인은 정확하게 실행할 수 있고 데이터 오류가 발생하지 않는 것이 바로 라인 안전이다.다음은 일반적인 스레드 코드를 살펴보겠습니다.
    public class ThreadTest {
    public static int index=0;
    public static String str="0";
    public static void main(String[] args) {
        ExecutorService pool1 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            pool1.execute(new Runnable() {
                @Override
                public void run() {
                    index++;
                    str="1";
                    System.out.println(index);
                    System.out.println(str);
                }
            });
        }
    }
    

    상기 코드에서 전체 index는 라인이 안전하지 않고 index++는 원자 조작이 아니며 그 조작은 주로 세 단계로 나뉜다.
  • index 값 0을 레지스터에 읽기
  • index의 값을 1
  • 더하기
  • index에 1을 부여
  • 다중 스레드 환경에서, 스레드 t1이 0을 로컬 메모리에 읽을 수 있지만, index의 값은 이미 매우 커졌다.String 클래스의 매개 변수str는 라인이 안전합니다. 왜냐하면 String은final로 장식되어 있기 때문입니다.라인 안전에 대해 말하자면 원자 조작, 원자 조작이란 같은 상태에 접근하는 모든 조작(이 조작 자체를 포함)에 대해 말하자면 이 조작은 원자 방식으로 실행되는 조작이다.통속적으로 말하면 이른바 원자란 분할할 수 없는 것이다. 예를 들어ConcurrentHashMap이containsKey 조작을 할 때 또 다른 라인이put 대상의 조작을 한다. 이렇게 하면ConcurrentHashMap의 내부 대상이 증가하고 그 상태가 바뀐다. 이것은 비원자적인 조작이다.index=0, 이것은 원자 조작으로 jvm의 모든 지령을 최소화합니다.일반적인 세 가지 동기화 메커니즘:
  • volatile 수식 구성원 변수,volatile는 경량의 동기화 메커니즘으로 모든 라인은 독립된 메모리를 가지고 서로 보이지 않는다.volatile 중의 하나는 공유 변수가 모든 라인에 대한 가시성을 확보하는 것이고 다른 역할은 명령의 재배열을 금지하는 것이다.
  • synchronized 수식 방법이나 동기화 코드 세션,synchronized 베이스는 모니터 모니터의 형식으로 라인의 안전을 제어한다. 한 라인이 들어갈 때 모니터 enter 명령이 있고 라인이 종료될 때 명령 모니터 exit를 촉발하고 다음 라인이 들어올 수 있다
  • ReentrantLock의 lock과 unlock 방법, ReentrantLock은 잠금장치와 잠금 해제를 분리하여 사용이 더욱 유연해지도록 했다. 그 밑바닥은 AbstractQueuedSynchronizer 병발기 제어 라인의 병발과 안전
  • 을 사용했다.
  • 라인 안전에 피해야 할 구덩이
  • 스레드 안전에서 흔히 볼 수 있는 구덩이는 다음과 같다.
  • 라인 내부에 국부 변수를 최대한 사용
  • 라인 내부는trycatch를 사용하여 이상을 포착해야 합니다. CountDownLatch를 사용하여 막으면finally에countDown 방법을 써야 합니다. 그렇지 않으면 이상을 던지면 프로그램이 계속 막혀서 아래로 진행할 수 없습니다
  • 스레드 풀을 사용할 때 CPU 코어의 2배에 달하는 스레드 수를 설정하지 마십시오
  • .
  • 스레드 내부의 집합은 시간에 따라 삽입, 삭제하지 마세요
  • 다중 스레드 내부에서 공유된 데이터 유형은 스레드가 안전하지 않으면 외부에서 사용할 때 잠금 동기화를 해야 한다. 예를 들어 ArrayList, HashMap 등이다.만약 라인이 안전하다면, 라인 내부에 복합 조작이 있는지 주의해야 한다.
  • ConcurrentHashMap에서 라인이 안전하지 않은 행위가 발생
  • 다음은 상기 구덩이의 다섯 번째 점에 중심을 두고 오늘의 주제인 ConcurrentHashMap의 라인이 안전하지 않은 행위, 왜 라인이 안전한 ConcurrentHashMap에서 라인이 안전하지 않은 행위가 나타나는지 코드에 직접 올린다.
    public class ThreadSafeTest {
    public static Map map=new ConcurrentHashMap<>();
    public static void main(String[] args) {
        ExecutorService pool1 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            pool1.execute(new Runnable() {
                @Override
                public void run() {
                    Random random=new Random();
                    int randomNum=random.nextInt(10);
                    if(map.containsKey(randomNum)){
                        map.put(randomNum,map.get(randomNum)+1);
                    }else{
                        map.put(randomNum,1);
                    }
                }
            });
        }
    }
    }
    

    이 코드는 10개의 스레드로 10 이내의 각 정형의 무작위 수가 나타나는 횟수를 테스트한 것으로 겉으로는 ConcurrentHashMap으로 contain과put 조작을 하는 데 아무런 문제가 없다.그러나 자세히 생각해 보면 contains Key와put 두 가지 방법은 모두 원자이지만 jvm에서 이 코드를 하나의 명령으로 집행하는 것이 아니다. 예를 들어 2개의 무작위 수 1을 연속적으로 생성한다고 가정하면 맵의contains Key와put 방법은 라인 A와 B가 동시에 집행한다. 그러면 A라인이 1put를 넣지 않았을 때 B라인은if의 조건을 판단하고 있다. 즉, 다음과 같은 집행 순서이다.
    A:  map         1    
    A    
    B:     map.containsKey(1)   false
    B:       1    map
    A:       1    map
    map  key  1  value      1
    

    이렇게 하면 두 번의 무작위 수 1이 생성되었지만value값은 1이므로 우리가 기대하는 결과는 2이어야 한다. 이것은 우리가 원하는 결과가 아니다.요약하면 두 라인이 동시에 맵을 경쟁하지만 맵에 대한 방문 순서는 먼저contains Key를 방문한 다음에put 대상이 들어가야 한다. 즉, 경쟁 조건이 생겼다.해결 방법은 당연히 동기화이다. 이제 우리는 코드를 다음과 같이 바꾸었다.
    public class ThreadSafeTest {
    public static Map map=new ConcurrentHashMap<>();
    public static void main(String[] args) {
        ExecutorService pool1 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            pool1.execute(new Runnable() {
                @Override
                public void run() {
                    Random random=new Random();
                    int randomNum=random.nextInt(10);
                   countRandom(randomNum);
                }
            });
        }
    }
    public static synchronized void countRandom(int randomNum){
        if(map.containsKey(randomNum)){
            map.put(randomNum,map.get(randomNum)+1);
        }else{
            map.put(randomNum,1);
        }
    }
    }
    

    상기 코드는 현재 클래스에서 루틴 안전에 문제가 없지만 루틴 안전의 위험이 있습니다. 구성원 변수 맵은 다른 곳에서 변경될 수 있습니다. 자바 병발 중 잘못된 동기화 자물쇠에 속합니다. countRandom을 다음과 같이 수정하면 됩니다.
     public static  void countRandom(int randomNum){
        synchronized(map){
            if(map.containsKey(randomNum)){
                map.put(randomNum,map.get(randomNum)+1);
            }else{
                map.put(randomNum,1);
            }
        }
    }
    

    상기 코드에서 동기화의 원인으로 인해 ConcurrentHashMap은 HashMap으로 바꿔도 되고 맵의 각 조작이 모두 라인이 안전하다는 것을 보증하면 된다.이 글을 쓰는 것도 제가 일하는 과정에서 겪은 버그입니다. 저는 현재 호텔 업계의 방 예약 업무에 종사하고 있습니다. 각 방마다 여러 개의 다른 제품이 판매되기 때문에 인터페이스를 통해 데이터를 얻을 때 이름이 같은 방을 하나의 제품으로 합쳐서 전시 판매를 해야 합니다. 예를 들어 다음과 같은 데이터입니다.
        {
            "roomId": 1,
            "roomName": "   ",
            "price": 1805
    
        }, {
    
            "roomId": 2,
            "roomName": "   ",
            "price": 1705
    
        }, {
    
            "roomId": 3,
            "roomName": "   ",
            "price": 1605
        }
    

    C단 사용자를 대상으로 각 주택 제품의 가격을 실시간으로 보여야 하기 때문에 다중 라인을 사용하고 ConcurrentHashMap을 사용했습니다. 그 중에서 키는 주택 이름 룸Name,value는 3개 주택 제품의 데이터이기 때문에 저는 라인 내부에서 다음과 같은 코드를 사용했습니다.
           if(map.containsKey(roomName)){
                map.put(roomName, map.get(roomName)+roomData2);
            }else{
                map.put(roomName,roomData);
            }
    

    회사 코드를 붙이기가 불편하기 때문에 상기 코드로 보여 드리겠습니다.논리란 맵에 이름이 같은 제품이 포함되어 있다면 그것을 꺼내서List에 넣고 put하는 것이다.결과적으로 데이터량이 많을 때 큰 침대방의 일부 가격이 덮여 전시되지 않아 우리의 제품 체험이 매우 나쁘다.마지막 해결 방법은 위의synchronized 키워드를 사용하여 맵을 동기화하는 것이다. 이렇게 하면 큰 침대방의 모든 가격이 전시되고 버그가 해결된다.
     
    2019-04-02 업데이트
    설명 영역에 ConcurrentHashMap의putIfAbsent 방법을 사용할 수 있다는 의견이 있습니다. 이 방법을 살펴보겠습니다.
     public V putIfAbsent(K key, V value) {
            return putVal(key, value, true);
        }
    /** Implementation for put and putIfAbsent */
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            if (key == null || value == null) throw new NullPointerException();
            int hash = spread(key.hashCode());
            int binCount = 0;
            for (Node[] tab = table;;) {
                Node f; int n, i, fh;
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    if (casTabAt(tab, i, null,
                                 new Node(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;
                                for (Node e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node pred = e;
                                    if ((e = e.next) == null) {
                                        pred.next = new Node(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            else if (f instanceof TreeBin) {
                                Node p;
                                binCount = 2;
                                if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }

    방법에서 볼 수 있어요.
     if (oldVal != null)
          return oldVal; 

    병렬 삽입할 때 원래의 값이 존재하면 바로 되돌아오고, 그렇지 않으면null로 되돌아옵니다.이것은 어떤 장면에서는 적합하지만, 내가 위에서 언급한 장면은 적합하지 않다
  • 스레드의 안전한 확장과 발산은 지금 발산합니다. 위에서 설명한 바와 같이 ConcurrentHashMap의 스레드가 안전하지 않은 행위를 말한다. 그러나 어떤 스레드가 안전한 데이터 유형이든 두 개의 스레드가 안전한 방법을 함께 사용하면 스레드가 안전하지 않은 행위를 할 수 있습니다. 예를 들어 List 동기화기 Vector의contains와dd 행위,스레드가 안전한 정형계수기 AtomicInteger의 get과incrementAndGet 행위 등등.
  • 귀납과 총결의 마지막 귀납 총결은 우리가 다중 스레드에서 스레드 안전 클래스를 사용할 때도 2개 이상의 동기화 방법을 사용했는지 주의해야 한다. 만약에 사용하면 이 여러 가지 방법을 동기화하여 원자 조작으로 만들어야 한다. 이른바 원자 조작은 jvm 내부의 지령으로 세분화되고 한 줄 코드가 원자 조작이라고 단순히 생각해서는 안 된다.절대로 라인 보안 데이터 형식만 사용하면 만사대길하다고 생각하지 마세요.
  • 좋은 웹페이지 즐겨찾기