JAVA并发编程工具

并发工具

Concurrent类型的容器

  • 内部很多操作使用cas优化,一般可以提供较高吞吐量
  • 弱一致性
    • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容时旧的
    • 求大小弱一致性,size操作未必时100%准确
    • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用fail-fast机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历

ConcurrentHashMap

  • 为什么要重写hashCode()和equals()

1.重复存储相同对象
若两个对象逻辑相等(根据业务规则,如字段值相同),但未重写 hashCode(),其默认哈希值(基于内存地址)不同。哈希集合(如 HashSet)会将其视为不同对象,导致重复存储.

Set<Person> set = new HashSet<>();
set.add(new Person("Alice", 30));  // 对象1
set.add(new Person("Alice", 30));  // 对象2(逻辑相同)
System.out.println(set.size());    // 输出 2(预期应为 1)

2.无法正确检索数据

HashMap 中,若用未重写 hashCode() 的对象作为键:
存储时:根据键的哈希值决定存储位置(桶索引)。
检索时:新键的哈希值不同,无法定位到原桶,返回 null

Map<Person, String> map = new HashMap<>();
map.put(new Person("Bob", 25), "Engineer");
String job = map.get(new Person("Bob", 25));  // 返回 null

3.性能下降/对象哈希值可能集中分布

  • JavaObject.hashCode() 默认实现基于对象的内存地址生成哈希值,哈希值分布依赖内存地址的分配模式,而非对象内容
  • JVM 分配内存时,若连续创建对象 obj1obj2obj3,其地址可能为 0x10000x10080x1010,哈希值对应为 100010081010(假设简化转换),默认哈希值的低位可能高度相似(如上述哈希值的低4位均为 000),导致计算出的桶索引集中在少数位置
    示例
对象内存地址哈希值桶索引 (15 & hash)
objA0x100040960 (因 4096 & 15 = 0)
objB0x200081920 (因 8192 & 15 = 0)
objC0x3000122880 (因 12288 & 15 = 0)

结果:所有对象被分配到索引0的桶中,冲突率100%

  • 集中分布的后果:多个对象被分配到同一桶中,HashMap 退化为链表(或红黑树),查询效率从 O(1) 降至 O(n)O(log n)

使用注意事项

复合操作非原子性问题

问题描述
单个操作(如 getput)是线程安全的,但多个操作的组合(如“检查再更新”)不保证原子性。例如:

// 线程不安全的库存扣减
public boolean processOrder(String productId, int quantity) {
    Integer stock = map.get(productId);
    if (stock >= quantity) {
        map.put(productId, stock - quantity); // 可能覆盖其他线程的修改
        return true;
    }
    return false;
}

风险:多个线程同时读取相同库存值并各自扣减,导致超卖(如实际库存 10,线程 A 和 B 均读到 10 并扣减 3 和 2,最终库存变为 7 而非 5)。

解决方案

使用原子方法:computeIfPresent()compute() 确保检查与更新原子化

map.computeIfPresent(productId, (k, v) -> v >= quantity ? v - quantity : v);

或配合 synchronizedReentrantLock 手动加锁(牺牲部分性能)。

弱一致性问题

问题描述

  • 迭代器弱一致性:迭代过程中若其他线程修改数据,迭代器可能反映部分修改或完全不反映,但不会抛 ConcurrentModificationException
  • size()/isEmpty() 不精确:返回的是近似值(无锁统计可能遗漏部分更新)。

::: tip
风险
实时统计场景(如实时监控大盘)可能读到过期数据
:::

  • 需强一致性的场景改用 Collections.synchronizedMap()Hashtable(性能更低)。 或通过额外锁机制同步
不支持 Null 键值

问题描述
键或值为 null 时会抛 NullPointerException(而 HashMap 允许)。
原因
多线程下无法区分“键不存在”和“键对应值为 null”,可能引发歧义。
解决方案

  • 改用 Optional 包装值(如 map.put(key, Optional.ofNullable(value)))。
  • 或确保业务逻辑中无需 null

使用示例

  • 示例一
// 使用 putIfAbsent:无论 "a" 是否存在,都会先创建 LongAdder 对象
conMap.putIfAbsent("a", new LongAdder());  // 可能浪费资源

// 使用 computeIfAbsent:仅当 "a" 不存在时才执行 new LongAdder()
LongAdder longAdder = counter.computeIfAbsent("key", k -> new LongAdder()); // 按需创建
longAdder.increment();  // +1

BlockingDeque

LinkedBlockingDeque

  • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行
      线程安全分析
  • 当节点总数大于2时(包括dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是
    head节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于2时(即一个dummy节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞
  • 当节点总数等于1时(就一个dummy节点)这时take线程会被notEmpty条件阻塞,有竞争,会阻塞

ArrayBlockingDeque

  • Linked支持有界,Array强制有界
  • Linked实现是链表,Array实现是数组
  • Linked是懒情的,而Array需要提前初始化Node数组
  • Linked每次入队会生成新Node,而ArrayNode是提前创建好的
  • Linked两把锁,Array一把锁

ConcurrentLinkedQueue

  • 两把【锁】,同一时刻,可以充许两个线程同时(一个生产者与一个消费者)执行
  • dummy节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
  • 只是这【锁】使用了cas来实现

CopyOnWriteArrayList / CopyOnWriteArraySet

  • 适合读多写少的场景,用空间换取线程安全

CopyOnlriteArraySetCopyOnWriteArrayList的马甲
底层实现采用了写入时贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。

  • get弱一致性
  • 迭代器弱一致性
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer>iter =list.iterator();
        new Thread(()-> {
            list.remove(0);
            System.out.println(list);
        }).start();
        Thread.sleep(1000);
        while (iter.hasNext()) {
            System.out.println(iter.next());
        } 

不要觉得弱一致性就不好

  • 数据库的MVCC都是弱一致性的表现(mysql的可重复读)
  • 并发高和一致性是矛盾的,需要权衡
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逻辑峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值