ConcurrentHashMap的使用场景

发布时间 2023-06-22 20:56:03作者: 哩个啷个波

一、并发容器ConcurrentHashMap

HashMap是我们用得非常频繁的一个集合,但是它是线程不安全的。并且在多线程环境下,put操作是有可能产生死循环,不过在JDK1.8的版本中更换了数据插入的顺序,已经解决了这个问题。

为了解决该问题,提供了Hashtable和Collections.synchronizedMap(hashMap)两种解决方案,但是这两种方案都是对读写加锁,独占式。一个线程在读时其他线程必须等待,吞吐量较低,性能较为低下。

而J.U.C给我们提供了高性能的线程安全HashMap:ConcurrentHashMap。

在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思路,而是利用CAS+Synchronized来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结构。

  ConcurrentHashMap通常只被看做并发效率更高的Map,用来替换其他线程安全的Map容器,比如 Hashtable和Collections.synchronizedMap。线程安全的容器,特别是Map,很多情况下一个业务中 涉及容器的操作有多个(读get写put,remove),即复合操作,而在并发执行时,线程安全的容器只能保证自身的数据不被破 坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确

1、ConcurrentHashMap对比Hashtable

Hashtable和ConcurrentHashMap的不同点:

(1)、Hashtable 对get,put,remove都使用了同步操作,它的同步级别是正对Hashtable来进行同步的,也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性能和吞吐量造成影响,从而形成单点。而ConcurrentHashMap则不同,它只对put,remove操作使用了同步操作,get操作并不影响。

(2)、Hashtable 在遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的话,就不用考虑这方面的问题了

2、ConcurrentHashMap总结

(1)、HashMap 是线程不安全的,ConcurrentHashMap是线程安全的,但是线程安全仅仅指的是对容器操作的时候是线程安全的

(2)、ConcurrentHashMap 的public V get(Object key)不涉及到锁,也就是说获得对象时没有使用锁,它只对put,remove操作使用了同步操作

(3)、put 、remove方法,在jdk7使用锁,但多线程中并不一定有锁争用,原因在于ConcurrentHashMap将缓存的变量分到多个Segment,每个Segment上有一个锁,只要多个线程访问的不是一个Segment就没有锁争用,就没有堵塞,各线程用各自的锁,ConcurrentHashMap缺省情况下生成16个Segment,也就是允许16个线程并发的更新而尽量没有锁争用。而在jdk8中使用的CAS+Synchronized来保证线程安全,比加锁的性能更高

(4)、ConcurrentHashMap 线程安全的,允许一边更新、一边遍历,也就是说在对象遍历的时候,也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化

以下例子分别使用HashMap、ConcurrentHashMap、HashTable在遍历的同时删除,

案例一:遍历的同时删除

说明:ConcurrentHashMap 线程安全的,允许一边更新、一边遍历,也就是说在对象遍历的时候,也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化

情况一、使用HashMap进行遍历的同时删除

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",1);
        map.put("c",1);
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            map.remove(entry.getKey());
        }
        System.out.println(map.size());
    }
}

HashMap不能一边遍历一边更新,否则报异常ConcurrentModificationException

img

情况二、使用ConcurrentHashMap进行遍历的同时删除

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
//        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> map = new ConcurrentHashMap<>();
        map.put("a",1);
        map.put("b",1);
        map.put("c",1);
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            map.remove(entry.getKey());
        }
        System.out.println(map.size());
    }
}

而ConcurrentHashMap不存在该问题,输出结果为0.

情况三、使用HashTable进行遍历的同时删除

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
//        Map<String, Integer> map = new HashMap<>();
//        Map<String, Integer> map = new ConcurrentHashMap<>();
        Map<String, Integer> map = new Hashtable<>();
        map.put("a",1);
        map.put("b",1);
        map.put("c",1);
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            map.remove(entry.getKey());
        }
        System.out.println(map.size());
    }
}

如果用性能较低的安全容器HashTable,也报异常ConcurrentModificationException。

img

案例2:业务操作的线程安全不能保证

说明:线程安全的容器,特别是Map,很多情况下一个业务中 涉及容器的操作有多个(读get写put,remove),即复合操作,而在并发执行时,线程安全的容器只能保证自身的数据不被破 坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确,即ConcurrentHashMap多线程操作不能保证数据同步

以下分别使用HashMap、ConcurrentHashMap、HashTable边遍历时边更新,运行了3个线程,理论上最后得到6000,

public class ConcurrentHashMapDemo2 {
    public static void main(String[] args) {
        final Map<String, Integer> count = new ConcurrentHashMap<>();
//        final Map<String, Integer> count = new HashMap<>();
//        final Map<String, Integer> count = new Hashtable<>();
        count.put("count",0);
        Runnable task = new Runnable(){
            @Override
            public void run() {
                int value;
                for (int i = 0; i < 2000; i++) {
                    value = count.get("count");
                    count.put("count",value + 1);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
        try {
            Thread.sleep(1000l);
            System.out.println(count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:

1、如果使用HashMap,结果为3426

{count=3426}

2、如果使用ConcurrentHashMap,结果为2525,因为只能保证对容器的操作是没问题的,但是不能保证业务是没有问题的,因为是复和操作且并发执行。

{count=2525}

3、HashTable也不能保证业务没有问题。

{count=3814}

如果非要在这种情况下保证线程安全问题,同步就可以了,加同步代码块,保证读写是同步的

public class ConcurrentHashMapDemo2 {
    public static void main(String[] args) {
        final Map<String, Integer> count = new ConcurrentHashMap<>();
//        final Map<String, Integer> count = new HashMap<>();
//        final Map<String, Integer> count = new Hashtable<>();
        count.put("count",0);
        Runnable task = new Runnable(){
            @Override
            public void run() {
                synchronized (count){
                    int value;
                    for (int i = 0; i < 2000; i++) {
                        value = count.get("count");
                        count.put("count",value + 1);
                    }
                }

            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
        try {
            Thread.sleep(1000l);
            System.out.println(count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果如下:

{count=6000}

案例3:多线程删除

public class ConcurrentHashMapDemo3 {
    public static void main(String[] args) {
//        final Map<String, Integer> count = new HashMap<>();
        final Map<String, Integer> count = new ConcurrentHashMap<>();
        //final Hashtable<String, Integer> count = new Hashtable<>();
        for (int i = 0; i < 2000; i++) {
            count.put("count" + i, 1);
        }
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    count.remove("count" + i);
                }
            }
        };
        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 1000; i < 1500; i++) {
                    count.remove("count" + i);
                }
            }
        };
        new Thread(task1).start();
        new Thread(task2).start();
        try {
            Thread.sleep(1000l);
            System.out.println(count.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

当使用ConcurrentHashMap时,结果为1000;当使用HashMap时,结果为1024;当使用HashTable时,结果为1000。

案例4:多线程遍历删除不同的的数据

public class ConcurrentHashMapDemo4 {
    public static void main(String[] args) {
        final Map<Integer, Integer> count = new ConcurrentHashMap<>();
        for (int i = 0; i < 2000; i++) {
            count.put(i, 1);
        }
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                Iterator<Map.Entry<Integer, Integer>> it = count.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry<Integer, Integer> entry = it.next();
                    if (entry.getKey() < 5) {
                        count.remove(entry.getKey()); // 多线程遍历的时候删除
                        System.out.println(entry.getKey());
                    }
                }
            }
        };
        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                Iterator<Map.Entry<Integer, Integer>> it = count.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry<Integer, Integer> entry = it.next();
                    if (entry.getKey() >= 1995) {
                        count.remove(entry.getKey()); // 多线程遍历的时候删除
                        System.out.println(entry.getKey());
                    }
                }
            }
        };
        new Thread(task1).start();
        new Thread(task2).start();
        try {
            Thread.sleep(1000l);
            System.out.println("map中键值对的数量"+count.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果如下:

0
1
2
3
4
1995
1996
1997
1998
1999
map中键值对的数量1990

推荐HashMap应用场景:

  多线程操作下HashMap无法保证数据同步,多线程修改HashMap并且有遍历的操作时,可能会产生ConcurrentModificationException异常。所以,推荐的HashMap应用场景是单线程运行环境,并且不需要遍历操作的场景。
这个推荐场景不是硬性条件。比如多线操作HashMap,我们通过加锁或者加入同步控制依然能正常应用HashMap,只是需要加上同步操作的代价。(单线程且不需要遍历时使用HashMap)

ConcurrentHashMap推荐应用场景:

多线程对HashMap数据添加删除操作时,可以采用ConcurrentHashMap。

下面三个场景都使用ConcurrentHashMap:

1、多线程添加或删除。2、遍历的时候删除。3、多线程遍历的时候删除数据

注意ConcurrentHashMap多线程操作不能保证数据同步,此时可以加同步代码块进行同步操作。

二、项目中ConcurrentHashMap的使用案例

在实际的生产环境中,单看代码很多操作并没有使用到我们常说的实现多线程的方式,但是结合具体的使用场景,某个接口或者方法会多次由不同请求发起时候,一个请求就会打开一个新的线程,其场景和直接使用多线程的效果差不多。

应用1:webSocket用来存放客户端的信息

(1)、建立连接后,把登录用户的uid和session通过put操作写入ConcurrentHashMap中。同时广播时要遍历ConcurrentHashMap来给每一个存活的session发消息该用户上线了。

session值如下:

img

sessionID为4,session中携带的值uid为2

(2)、登录用户下线后,需要遍历ConcurrentHashMap,如果ConcurrentHashMap中的sessionID等于当前下线的用户的sessionID,则从ConcurrentHashMap中移出。然后广播时再遍历ConcurrentHashMap,然后给每一个Session发送该用户下线的消息。

/**
 * Socket处理器
 */
@Component
public class MyWebSocketHandler implements WebSocketHandler {
    //用于保存HttpSession与WebSocketSession的映射关系
    public static final Map<Long, WebSocketSession> userSocketSessionMap;

    @Autowired
    LoginService loginservice;

    static {
        userSocketSessionMap = new ConcurrentHashMap<Long, WebSocketSession>();
    }

    /**
     * 建立连接后,把登录用户的id写入WebSocketSession
     */
    public void afterConnectionEstablished(WebSocketSession session)
            throws Exception {
        Long uid = (Long) session.getAttributes().get("uid");
        String username=loginservice.getnamebyid(uid);
        if (userSocketSessionMap.get(uid) == null) {
            userSocketSessionMap.put(uid, session); // 多线程添加操作
            Message msg = new Message();
            msg.setFrom(0L);//0表示上线消息
            msg.setText(username);
            this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
        }
    }

    /**
     * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
     */
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
            if(message.getPayloadLength()==0)
                return;
            Message msg=new Gson().fromJson(message.getPayload().toString(),Message.class);
            msg.setDate(new Date());
            sendMessageToUser(msg.getTo(), new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
    }

    /**
     * 消息传输错误处理
     */
    public void handleTransportError(WebSocketSession session,
            Throwable exception) throws Exception {
        if (session.isOpen()) {
            session.close();
        }
        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();
        // 移除当前抛出异常用户的Socket会话
        while (it.hasNext()) {
            Entry<Long, WebSocketSession> entry = it.next();
            if (entry.getValue().getId().equals(session.getId())) {
                userSocketSessionMap.remove(entry.getKey());
                System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
                String username=loginservice.getnamebyid(entry.getKey());
                Message msg = new Message();
                msg.setFrom(-2L);
                msg.setText(username);
                this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
                break;
            }
        }
    }

    /**
     * 关闭连接后
     */
    public void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception {
        System.out.println("Websocket:" + session.getId() + "已经关闭");
        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();
        // 移除当前用户的Socket会话
        while (it.hasNext()) {
            Entry<Long, WebSocketSession> entry = it.next();
            if (entry.getValue().getId().equals(session.getId())) {
                userSocketSessionMap.remove(entry.getKey()); // 多线程遍历的时候删除
                System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
                String username=loginservice.getnamebyid(entry.getKey());
                Message msg = new Message();
                msg.setFrom(-2L);//下线消息,用-2表示
                msg.setText(username);
                this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
                break;
            }
        }
    }

    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 给所有在线用户发送消息
     * @param message
     * @throws IOException
     */
    public void broadcast(final TextMessage message) throws IOException {
        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();

        //多线程群发
        while (it.hasNext()) {

            final Entry<Long, WebSocketSession> entry = it.next();

            if (entry.getValue().isOpen()) {
                // entry.getValue().sendMessage(message);
                new Thread(new Runnable() {

                    public void run() {
                        try {
                            if (entry.getValue().isOpen()) {
                                entry.getValue().sendMessage(message);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }).start();
            }

        }
    }

    /**
     * 给某个用户发送消息
     *
     * @param uid
     * @param message
     * @throws IOException
     */
    public void sendMessageToUser(Long uid, TextMessage message) throws IOException {
        WebSocketSession session = userSocketSessionMap.get(uid);
        if (session != null && session.isOpen()) {
            session.sendMessage(message);
        }
    }

}