ZetCode

Java Collections.synchronizedSet 方法

上次修改时间:2025 年 4 月 20 日

Collections.synchronizedSet 方法是 Java Collections Framework 中的一个实用方法。它返回一个由指定的 set 支持的同步(线程安全)的 set。这个包装器为基本操作提供线程安全保障。

对返回的 set 的所有访问都必须通过同步包装器进行。该方法确保单个操作是原子且线程安全的。但是,复合操作需要外部同步。

Collections.synchronizedSet 概述

synchronizedSet 方法是 java.util.Collections 类的一部分。它接受一个 Set 作为输入,并返回一个线程安全的版本。返回的 set 的迭代器需要手动同步以保证线程安全。

当您需要线程安全性,但又不想使用基于 ConcurrentHashMap 的 set 时,此方法很有用。它更简单,但与并发集合相比,在高并发情况下性能可能较低。

基本 synchronizedSet 示例

此示例演示了如何从 HashSet 创建一个同步的 set。我们执行添加、删除和包含等基本操作。该示例展示了基本的线程安全包装器用法。

SynchronizedSetBasic.java
package com.zetcode;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SynchronizedSetBasic {

    public static void main(String[] args) {
        
        Set<String> normalSet = new HashSet<>();
        Set<String> syncSet = Collections.synchronizedSet(normalSet);
        
        // Add elements to synchronized set
        syncSet.add("Apple");
        syncSet.add("Banana");
        syncSet.add("Cherry");
        
        // Check size and contents
        System.out.println("Size: " + syncSet.size());
        System.out.println("Contains Banana: " + syncSet.contains("Banana"));
        
        // Remove element
        syncSet.remove("Apple");
        System.out.println("After removal: " + syncSet);
    }
}

此代码创建一个围绕 HashSet 的同步 set 包装器。对同步 set 的所有操作都是线程安全的。该示例展示了现在受到保护,免受并发访问的基本操作。

输出结果表明,同步 set 在单线程操作中表现得像一个普通 set。线程安全在多线程场景中变得明显。

多线程访问示例

此示例演示了 synchronizedSet 如何处理来自多个线程的并发访问。我们创建了几个同时修改 set 的线程。同步包装器可以防止数据损坏。

SynchronizedSetThreads.java
package com.zetcode;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SynchronizedSetThreads {

    public static void main(String[] args) throws InterruptedException {
        
        Set<Integer> numbers = Collections.synchronizedSet(new HashSet<>());
        
        // Create and start multiple threads
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    numbers.add(threadId * 1000 + j);
                }
            });
            threads[i].start();
        }
        
        // Wait for all threads to complete
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println("Final set size: " + numbers.size());
    }
}

此示例显示了 synchronizedSet 的线程安全特性。多个线程并发添加元素而不会导致损坏。每个添加操作都是原子的,并受到同步的保护。

最终大小应为 5000(5 个线程 × 1000 次添加)。如果没有同步,由于更新丢失,大小可能会更小。

迭代同步 Set

迭代同步 set 需要手动同步。此示例演示了在保持线程安全的同时进行迭代的正确方法。如果没有外部同步,迭代器本身不是线程安全的。

SynchronizedSetIteration.java
package com.zetcode;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SynchronizedSetIteration {

    public static void main(String[] args) {
        
        Set<String> colors = Collections.synchronizedSet(new HashSet<>());
        colors.add("Red");
        colors.add("Green");
        colors.add("Blue");
        
        // Proper synchronized iteration
        synchronized(colors) {
            for (String color : colors) {
                System.out.println(color);
            }
        }
        
        // Unsafe iteration (may throw ConcurrentModificationException)
        try {
            for (String color : colors) {
                System.out.println(color);
            }
        } catch (Exception e) {
            System.out.println("Exception during unsafe iteration: " + e);
        }
    }
}

该示例展示了安全和不安全的迭代方法。同步块确保在迭代期间不会发生修改。如果没有同步,并发修改可能会导致异常。

在迭代同步集合时,始终使用手动同步。这可以防止 ConcurrentModificationException 并确保一致的视图。

复合操作示例

对同步 set 的复合操作需要额外的同步。此示例演示了如何安全地执行“检查然后执行”操作。单个操作是线程安全的,但序列需要保护。

SynchronizedSetCompound.java
package com.zetcode;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SynchronizedSetCompound {

    public static void main(String[] args) {
        
        Set<String> users = Collections.synchronizedSet(new HashSet<>());
        
        // Unsafe compound operation
        if (!users.contains("Admin")) {
            users.add("Admin"); // Race condition possible
        }
        
        // Safe compound operation
        synchronized(users) {
            if (!users.contains("Admin")) {
                users.add("Admin");
            }
        }
        
        System.out.println("Users: " + users);
    }
}

该示例展示了复合操作的不安全和安全版本。不安全版本可能仍允许重复添加,这是由于竞态条件导致的。安全版本使用外部同步来保护整个操作序列。

执行必须是原子的多个操作时,始终手动同步。synchronized set 仅保护单个方法调用。

性能比较

此示例比较了 synchronizedSet 与常规 HashSet 和 ConcurrentHashMap 的 keySet 的性能。它演示了同步的开销。结果会因系统和争用级别的不同而异。

SynchronizedSetPerformance.java
package com.zetcode;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class SynchronizedSetPerformance {

    public static void main(String[] args) {
        
        final int ITERATIONS = 1_000_000;
        
        Set<Integer> hashSet = new HashSet<>();
        Set<Integer> syncSet = Collections.synchronizedSet(new HashSet<>());
        Set<Integer> concurrentSet = ConcurrentHashMap.newKeySet();
        
        // Test HashSet (unsynchronized)
        long start = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            hashSet.add(i);
        }
        System.out.println("HashSet time: " + 
            (System.currentTimeMillis() - start) + "ms");
        
        // Test synchronizedSet
        start = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            syncSet.add(i);
        }
        System.out.println("synchronizedSet time: " + 
            (System.currentTimeMillis() - start) + "ms");
        
        // Test ConcurrentHashMap's keySet
        start = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            concurrentSet.add(i);
        }
        System.out.println("ConcurrentSet time: " + 
            (System.currentTimeMillis() - start) + "ms");
    }
}

此性能测试显示了不同线程安全 set 实现的相对开销。在争用情况下,synchronizedSet 通常显示比 ConcurrentHashMap 的 set 更高的开销。

结果有助于根据性能要求在同步方法之间进行选择。对于读取繁重的工作负载,synchronizedSet 可能足够了。

真实世界的用法示例

此示例演示了 synchronizedSet 的一个实际用例——跟踪 Web 应用程序中的活动用户会话。由于会话是并发添加/删除的,因此 set 需要是线程安全的。

SessionTracker.java
package com.zetcode;

import java.util.Collections;
import java.util.Set;
import java.util.HashSet;

public class SessionTracker {

    private final Set<String> activeSessions = 
        Collections.synchronizedSet(new HashSet<>());
    
    public void addSession(String sessionId) {
        activeSessions.add(sessionId);
        System.out.println("Session added: " + sessionId);
    }
    
    public void removeSession(String sessionId) {
        activeSessions.remove(sessionId);
        System.out.println("Session removed: " + sessionId);
    }
    
    public int getActiveSessionCount() {
        return activeSessions.size();
    }
    
    public static void main(String[] args) {
        SessionTracker tracker = new SessionTracker();
        
        // Simulate concurrent session activity
        new Thread(() -> {
            tracker.addSession("user1");
            tracker.addSession("user2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            tracker.removeSession("user1");
        }).start();
        
        new Thread(() -> {
            tracker.addSession("user3");
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            tracker.removeSession("user3");
            tracker.addSession("user4");
        }).start();
        
        try { Thread.sleep(200); } catch (InterruptedException e) {}
        System.out.println("Active sessions: " + tracker.getActiveSessionCount());
    }
}

此示例模拟了 Web 应用程序会话跟踪器。同步 set 确保了线程安全的会话管理。多个线程可以安全地并发添加和删除会话。

输出结果显示了会话被添加和删除。最终计数正确地反映了剩余的活动会话。这种模式在服务器应用程序中很常见。

来源

Java Collections.synchronizedSet 文档

在本教程中,我们深入探讨了 Collections.synchronizedSet 方法。我们涵盖了基本用法、线程安全、迭代、复合操作、性能和实际应用。这个包装器为 set 操作提供了简单的线程安全保障。

作者

我的名字是 Jan Bodnar,是一位经验丰富的程序员,在编程领域拥有多年的经验。我从 2007 年开始撰写编程文章,此后撰写了 1,400 多篇文章和八本电子书。凭借超过八年的教学经验,我致力于分享我的知识并帮助他人掌握编程概念。

列出所有Java教程