25、深入并发数据结构和同步工具

深入并发数据结构和同步工具

在 Java 编程中,并发数据结构和同步工具是处理多线程编程的重要组成部分。Java 8 为这些并发数据结构引入了许多新特性,下面将详细介绍这些新特性及其使用方法。

1. 并发数据结构接口和类

Java 并发 API 提供了多种接口和类来处理并发数据。以下是一些重要的接口和类:
- TransferQueue 接口 :扩展了 BlockingQueue 接口,添加了从生产者向消费者传输元素的方法。
- transfer() :将元素传输给消费者,并阻塞调用线程直到元素被消费。
- tryTransfer() :如果有消费者在等待,则传输元素;否则返回 false,且不将元素插入队列。
- 各类实现类
| 类名 | 特点 |
| ---- | ---- |
| LinkedBlockingQueue | 实现 BlockingQueue 接口,提供可选有限元素数量的阻塞队列,还实现了 Queue、Collection 和 Iterable 接口。 |
| ConcurrentLinkedQueue | 实现 Queue 接口,提供线程安全的无界队列,内部使用非阻塞算法。 |
| LinkedBlockingDeque | 实现 BlockingDeque 接口,提供可选有限元素数量的双端阻塞队列,功能比 LinkedBlockingQueue 多,但可能有更多开销。 |
| ConcurrentLinkedDeque | 实现 Deque 接口,提供线程安全的无界双端队列,允许在两端添加和删除元素。 |
| ArrayBlockingQueue | 实现 BlockingQueue 接口,基于数组提供有限元素数量的阻塞队列,构造时指定数组大小且不调整。 |
| DelayQueue | 实现 BlockingDeque 接口,提供无界阻塞队列,元素必须实现 Delayed 接口,根据延迟时间出队。 |
| LinkedTransferQueue | 实现 TransferQueue 接口,提供无界阻塞队列,可作为生产者和消费者的通信通道。 |
| PriorityBlockingQueue | 实现 BlockingQueue 接口,元素可按自然顺序或指定比较器出队。 |
| ConcurrentHashMap | 实现 ConcurrentMap 接口,提供线程安全的哈希表,Java 8 增加了新方法。 |

2. ConcurrentHashMap 的新方法

ConcurrentHashMap 在 Java 8 中添加了许多新方法,下面详细介绍几个重要的方法。

2.1 forEach() 方法

该方法允许对 ConcurrentHashMap 的每个 (key, value) 对执行指定函数。
- 基本版本

productsByBuyer.forEach( (id, list) ->  
    System.out.println(id+": "+list.size()));

此基本版本是 Map 接口的一部分,始终顺序执行。
- 计算平均评分示例

productsByBuyer.forEach( (id, list) -> {
    double average=list.stream().mapToDouble(item ->  
        item.getValue()).average().getAsDouble();
    System.out.println(id+": "+average);
});
  • 其他版本
    • forEach(parallelismThreshold, action) :用于并发应用,当元素数量超过阈值时并行执行。
    • forEachEntry(parallelismThreshold, action) :操作接收 Map.Entry 对象。
    • forEachKey(parallelismThreshold, action) :仅对键执行操作。
    • forEachValue(parallelismThreshold, action) :仅对值执行操作。
2.2 search() 方法

该方法对 ConcurrentHashMap 的所有元素应用搜索函数,返回第一个非空结果。

graph TD;
    A[开始] --> B[检查元素数量是否超阈值];
    B -- 是 --> C[并行执行搜索函数];
    B -- 否 --> D[顺序执行搜索函数];
    C --> E[返回第一个非空结果];
    D --> E;
    E --> F[结束];
  • 示例代码
ExtendedProduct firstProduct=productsByBuyer.search(100,
    (id, products) -> {
        for (ExtendedProduct product: products) {
            if (product.getTitle()  
                .toLowerCase().contains("java")) {
                return product;
            }
        }
        return null;
    });
if (firstProduct!=null) {
    System.out.println(firstProduct.getBuyer()+":"+  
        firstProduct.getTitle());
}
  • 其他版本
    • searchEntries(parallelismThreshold, searchFunction) :搜索函数接收 Map.Entry 对象。
    • searchKeys(parallelismThreshold, searchFunction) :仅对键应用搜索函数。
    • searchValues(parallelismThreshold, searchFunction) :仅对值应用搜索函数。
2.3 reduce() 方法

类似于 Stream 框架的 reduce() 方法,直接处理 ConcurrentHashMap 的元素。

graph TD;
    A[开始] --> B[检查元素数量是否超阈值];
    B -- 是 --> C[并行执行转换和合并操作];
    B -- 否 --> D[顺序执行转换和合并操作];
    C --> E[返回合并结果];
    D --> E;
    E --> F[结束];
  • 示例代码
BiFunction<String, List<ExtendedProduct>, List<ExtendedProduct>>  
    transformer = (key, value) -> value.stream().filter(product ->  
        product.getValue() == 1).collect(Collectors.toList());
BinaryOperator<List<ExtendedProduct>> reducer = (list1, list2) ->{
    list1.addAll(list2);
    return list1;
};
List<ExtendedProduct> badReviews=productsByBuyer.reduce(10,  
    transformer, reducer);
badReviews.forEach(product -> {
    System.out.println(product.getTitle()+":"+  
        product.getBuyer()+":"+product.getValue());
});
  • 其他版本
    • reduceEntries() 等:对 Map.Entry 对象执行操作。
    • reduceKeys() 等:对键执行操作。
    • reduceToInt() 等:返回不同类型的结果。
    • reduceValues() 等:对值执行操作。
2.4 compute() 方法

该方法根据键和 BiFunction 接口的实现来更新、插入或删除元素。

ConcurrentHashMap<String, LongAdder> counter=new  
    ConcurrentHashMap<>();
badReviews.forEach(product -> {
    counter.computeIfAbsent(product.getTitle(), title -> new  
        LongAdder()).increment();
});
counter.forEach((title, count) -> {
    System.out.println(title+":"+count);
});
2.5 merge() 方法

用于合并 (key, value) 对到 Map 中。

Path path=Paths.get("data\\amazon\\1995.txt");
ConcurrentHashMap<BasicProduct,  
    ConcurrentLinkedDeque<BasicReview>>  
    products1995=BasicProductLoader.load(path);
path=Paths.get("data\\amazon\\1996.txt");
ConcurrentHashMap<BasicProduct,  
    ConcurrentLinkedDeque<BasicReview>>  
    products1996=BasicProductLoader.load(path);
products1996.forEach(10,(product, reviews) -> {
    products1995.merge(product, reviews, (reviews1,  
        reviews2) -> {
        System.out.println("Merge for:  
            "+product.getAsin());
        reviews1.addAll(reviews2);
        return reviews1;
    });
});
3. ConcurrentLinkedDeque 类的新方法

Collection 接口在 Java 8 中添加了新方法,ConcurrentLinkedDeque 可使用这些方法。

3.1 removeIf() 方法

根据 Predicate 接口的实现删除元素。

System.out.println("Products: "+productList.size());
productList.removeIf(product -> product.getSalesrank() >  
    1000);
System.out.println("Products; "+productList.size());
productList.forEach(product -> {
    System.out.println(product.getTitle()+":  
        "+product.getSalesrank());
});
3.2 spliterator() 方法

返回 Spliterator 接口的实现,可用于创建自定义流源。

public class SpliteratorTask implements Runnable {
    private Spliterator<Product> spliterator;
    public SpliteratorTask (Spliterator<Product> spliterator) {
        this.spliterator=spliterator;
    }
    @Override
    public void run() {
        int counter=0;
        while (spliterator.tryAdvance(product -> {
            product.setTitle(product.getTitle().toLowerCase());
        })) {
            counter++;
        };
        System.out.println(Thread.currentThread().getName()  
            +":"+counter);
    }
}
Spliterator<Product> split1=productList.spliterator();
System.out.println(split1.hasCharacteristics  
    (Spliterator.CONCURRENT));
System.out.println(split1.hasCharacteristics  
    (Spliterator.SUBSIZED));
System.out.println(split1.estimateSize());
Spliterator<Product> split2=split1.trySplit();
System.out.println(split1.estimateSize());
System.out.println(split2.estimateSize());
ThreadPoolExecutor executor=(ThreadPoolExecutor)  
    Executors.newCachedThreadPool();
executor.execute(new SpliteratorTask(split1));
executor.execute(new SpliteratorTask(split2));
4. 原子变量

Java 1.5 引入了原子变量,Java 8 又添加了四个新类:DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder。这些类提供了更好的性能,适用于多线程频繁更新累加和的场景。
- 重要方法
- add() :按指定值增加计数器。
- increment() :等同于 add(1)
- decrement() :等同于 add(-1)
- sum() :返回计数器的当前值。

通过以上介绍,我们了解了 Java 8 为并发数据结构引入的新特性,这些特性可以帮助我们更高效地处理多线程编程中的数据操作。

深入并发数据结构和同步工具(续)

5. 各方法的使用总结与对比

为了更清晰地理解和使用上述介绍的方法,下面对 ConcurrentHashMap 和 ConcurrentLinkedDeque 中的重要方法进行总结和对比。

数据结构 方法 功能描述 适用场景
ConcurrentHashMap forEach() 对每个 (key, value) 对执行指定函数 遍历并处理所有元素,如统计每个用户的购买数量或平均评分
search() 应用搜索函数,返回第一个非空结果 查找满足特定条件的第一个元素
reduce() 对元素进行转换和合并操作 聚合元素,如收集特定评分的产品列表
compute() 根据键和函数更新、插入或删除元素 对特定键的值进行复杂计算和更新
merge() 合并 (key, value) 对到 Map 中 合并两个 Map 中的元素
ConcurrentLinkedDeque removeIf() 根据 Predicate 接口的实现删除元素 删除满足特定条件的元素
spliterator() 返回 Spliterator 接口的实现,用于创建自定义流源 并发处理集合元素
6. 实际应用案例分析

下面通过一个实际的应用案例,展示如何综合运用上述方法来解决实际问题。

假设我们有一个电商系统,需要统计每个商品的好评和差评数量,并找出好评率最高的商品。

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.Collectors;

class Product {
    private String id;
    private String title;
    private int value;

    public Product(String id, String title, int value) {
        this.id = id;
        this.title = title;
        this.value = value;
    }

    public String getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public int getValue() {
        return value;
    }
}

public class EcommerceAnalysis {
    public static void main(String[] args) {
        // 模拟商品评价数据
        ConcurrentHashMap<String, ConcurrentLinkedDeque<Product>> productReviews = new ConcurrentHashMap<>();
        for (int i = 0; i < 100; i++) {
            String productId = "P" + i;
            ConcurrentLinkedDeque<Product> reviews = new ConcurrentLinkedDeque<>();
            for (int j = 0; j < 10; j++) {
                int rating = new Random().nextInt(5) + 1;
                reviews.add(new Product(productId, "Product " + i, rating));
            }
            productReviews.put(productId, reviews);
        }

        // 统计每个商品的好评和差评数量
        ConcurrentHashMap<String, LongAdder> goodReviewsCount = new ConcurrentHashMap<>();
        ConcurrentHashMap<String, LongAdder> badReviewsCount = new ConcurrentHashMap<>();

        productReviews.forEach((productId, reviews) -> {
            reviews.forEach(review -> {
                if (review.getValue() >= 4) {
                    goodReviewsCount.computeIfAbsent(productId, k -> new LongAdder()).increment();
                } else {
                    badReviewsCount.computeIfAbsent(productId, k -> new LongAdder()).increment();
                }
            });
        });

        // 计算每个商品的好评率
        Map<String, Double> goodReviewRate = new HashMap<>();
        goodReviewsCount.forEach((productId, goodCount) -> {
            long totalReviews = goodCount.sum() + badReviewsCount.getOrDefault(productId, new LongAdder()).sum();
            double rate = (double) goodCount.sum() / totalReviews;
            goodReviewRate.put(productId, rate);
        });

        // 找出好评率最高的商品
        String bestProductId = goodReviewRate.entrySet().stream()
               .max(Map.Entry.comparingByValue())
               .map(Map.Entry::getKey)
               .orElse(null);

        if (bestProductId != null) {
            System.out.println("好评率最高的商品 ID: " + bestProductId + ", 好评率: " + goodReviewRate.get(bestProductId));
        }
    }
}

在这个案例中,我们使用了 ConcurrentHashMap 来存储商品评价数据,使用 LongAdder 来统计好评和差评数量。通过 forEach 方法遍历所有评价数据,使用 computeIfAbsent 方法更新计数器。最后,使用 Stream API 找出好评率最高的商品。

7. 性能优化建议

在使用并发数据结构和同步工具时,为了提高性能,我们可以遵循以下建议:

  • 合理设置并行阈值 :在使用并行方法(如 forEach(parallelismThreshold, action)、search(parallelismThreshold, searchFunction) 等)时,根据数据规模和系统资源合理设置并行阈值。如果数据量较小,并行执行可能会带来额外的开销,此时可以选择顺序执行。
  • 避免锁竞争 :在使用 compute() 方法时,要注意避免长时间的操作和对其他元素的更新,以免造成锁竞争和死锁。尽量将复杂的计算和操作放在方法外部进行。
  • 选择合适的数据结构 :根据实际需求选择合适的并发数据结构。例如,如果需要一个无界的线程安全队列,可以选择 ConcurrentLinkedQueue;如果需要一个有界的阻塞队列,可以选择 ArrayBlockingQueue。
8. 总结与展望

通过本文的介绍,我们深入了解了 Java 8 为并发数据结构引入的新特性,包括 ConcurrentHashMap 和 ConcurrentLinkedDeque 中的重要方法,以及原子变量的使用。这些新特性为我们处理多线程编程中的数据操作提供了更强大的工具和更高效的方式。

在未来的 Java 开发中,随着多核处理器的普及和并发编程的需求不断增加,并发数据结构和同步工具将发挥更加重要的作用。我们可以期待 Java 会继续完善和优化这些功能,为开发者提供更多的便利和更好的性能。同时,开发者也应该不断学习和掌握这些新特性,以提高自己的并发编程能力,开发出更加高效、稳定的 Java 应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值