무시하기 쉬운 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++는 원자 조작이 아니며 그 조작은 주로 세 단계로 나뉜다.
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로 되돌아옵니다.이것은 어떤 장면에서는 적합하지만, 내가 위에서 언급한 장면은 적합하지 않다