← Назад к вопросам

Является ли Vector не потокобезопасным?

1.3 Junior🔥 141 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Является ли Vector не потокобезопасным?

Это классический вопрос на интервью, который показывает понимание многопоточности в Java. Ответ контринтуитивный: Vector ЯВЛЯЕТСЯ потокобезопасным, но это частая причина ошибок. Давайте разберёмся почему.

Что такое Vector?

Vector — это синхронизированная версия ArrayList, которая была добавлена в Java 1.0:

// Vector — это legacy класс, который синхронизирован
public class Vector<E> extends AbstractList<E> 
    implements List<E>, RandomAccess, Cloneable, Serializable {
    
    // Все методы синхронизированы
    public synchronized boolean add(E e) {
        // ...
    }
    
    public synchronized E get(int index) {
        // ...
    }
    
    public synchronized boolean remove(Object o) {
        // ...
    }
}

Vector ЯВЛЯЕТСЯ потокобезопасным

На первый взгляд Vector потокобезопасен:

public class VectorExample {
    static Vector<Integer> vector = new Vector<>();
    
    public static void main(String[] args) throws InterruptedException {
        // Два потока добавляют элементы
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                vector.add(i);
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                vector.add(i);
            }
        });
        
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        
        System.out.println("Size: " + vector.size()); // 2000
    }
}

Все методы Vector синхронизированы с помощью synchronized блоков.

Но есть скрытая проблема: compound operations

Проблема возникает при сложных операциях (compound operations):

public class VectorRaceCondition {
    static Vector<String> vector = new Vector<>();
    static boolean elementFound = false;
    
    public static void main(String[] args) throws InterruptedException {
        vector.add("item1");
        vector.add("item2");
        vector.add("item3");
        
        // Поток 1: проверяет наличие элемента
        Thread thread1 = new Thread(() -> {
            // RACE CONDITION: между contains и remove
            if (vector.contains("item1")) { // 1. Проверка
                // Между этими двумя линиями может произойти всё что угодно
                Thread.yield(); // Имитируем задержку
                vector.remove("item1"); // 2. Удаление
                elementFound = true;
            }
        });
        
        // Поток 2: также удаляет элемент
        Thread thread2 = new Thread(() -> {
            vector.remove("item1"); // Может удалить уже удаленный элемент
        });
        
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

Визуализация race condition:

Поток 1:
├─ t1: vector.contains("item1")  → true (блокировка синхро)
├─ [Отпустил lock]
├─ t3: vector.remove("item1")   → успешно удален
└─ Проблема: мог удалить элемент, который был удален в т4

Поток 2:
├─ t2: [ждёт lock]
├─ t4: vector.remove("item1")   → выполнилась между t1 и t3
└─ Проблема: удаляет то же самое

Результат: элемент удалён дважды, или выброшено исключение

Реальный пример проблемы

public class VectorProblemExample {
    static Vector<Integer> numbers = new Vector<>();
    
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            numbers.add(i);
        }
        
        // Поток 1: итерирует и удаляет
        Thread thread1 = new Thread(() -> {
            try {
                // Ошибка: ConcurrentModificationException
                for (int i = 0; i < numbers.size(); i++) { // size() может измениться
                    int value = numbers.get(i);
                    if (value % 2 == 0) {
                        numbers.remove(i); // Индекс сместился!
                    }
                }
            } catch (Exception e) {
                System.out.println("Exception: " + e);
            }
        });
        
        // Поток 2: также добавляет элементы
        Thread thread2 = new Thread(() -> {
            for (int i = 100; i < 200; i++) {
                numbers.add(i);
            }
        });
        
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

Почему Vector считается неправильным выбором

1. Синхронизация на методе — очень грубая

// Vector синхронизирует ВЕСЬ метод
public synchronized boolean add(E e) {
    // Весь этот код защищен одной блокировкой
    // Это неэффективно
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

2. Производительность хуже из-за синхронизации

public class PerformanceComparison {
    public static void main(String[] args) {
        // ArrayList (не синхронизирован)
        ArrayList<Integer> list = new ArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) {
            list.add(i);
        }
        long arrayListTime = System.nanoTime() - start;
        
        // Vector (синхронизирован)
        Vector<Integer> vector = new Vector<>();
        start = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) {
            vector.add(i);
        }
        long vectorTime = System.nanoTime() - start;
        
        System.out.println("ArrayList: " + arrayListTime);
        System.out.println("Vector: " + vectorTime);
        // Vector будет примерно в 2-3 раза медленнее
    }
}

3. Ложное чувство безопасности

public class FalseSecurityExample {
    Vector<String> vector = new Vector<>();
    
    // Разработчик думает что это потокобезопасно
    public void safeOperation() {
        if (vector.size() > 0) {                    // 1. size() синхронизирован
            String element = vector.get(0);         // 2. get() синхронизирован
            // НО между шагами 1 и 2 элемент может быть удален другим потоком!
        }
    }
}

Правильный способ: Collections.synchronizedList

public class ProperSynchronization {
    // Обёртка для ArrayList
    List<String> list = Collections.synchronizedList(new ArrayList<>());
    
    // Но и это не решает проблему compound operations!
    public void example() {
        if (list.contains("item")) {
            list.remove("item"); // Всё ещё race condition!
        }
    }
}

Лучший способ: CopyOnWriteArrayList (для чтения)

public class CopyOnWriteExample {
    // Для случаев когда чтение частое, запись редкая
    List<String> list = new CopyOnWriteArrayList<>();
    
    public void readHeavy() {
        // Отлично для частого чтения
        for (String item : list) {
            System.out.println(item);
        }
    }
}

Лучший способ: явная синхронизация

public class ExplicitSynchronization {
    List<String> list = new ArrayList<>(); // Не синхронизирован
    Object lock = new Object();
    
    // Compound operation защищена одной блокировкой
    public void safeRemoveIfExists(String item) {
        synchronized (lock) {
            if (list.contains(item)) {
                list.remove(item);
            }
        }
    }
    
    // Или:
    public void safeRemoveIfExists2(String item) {
        synchronized (list) { // Использовать сам список как lock
            if (list.contains(item)) {
                list.remove(item);
            }
        }
    }
}

Лучший способ: явная итерация

public class ExplicitIteration {
    List<String> list = new CopyOnWriteArrayList<>();
    
    public void safeIteration() {
        // CopyOnWriteArrayList создаёт snapshot
        for (String item : list) {
            System.out.println(item);
        }
    }
    
    // Или с ArrayList + синхронизацией:
    List<String> arrayList = new ArrayList<>();
    Object lock = new Object();
    
    public void safeIterationWithLock() {
        synchronized (lock) {
            for (String item : arrayList) {
                System.out.println(item);
            }
        }
    }
}

Матрица выбора Collection для многопотока

┌──────────────────────────────────────────────────────────┐
│ Много чтения, редко запись                              │
├──────────────────────────────────────────────────────────┤
│ ✓ CopyOnWriteArrayList                                   │
│ ✓ Collections.synchronizedList + явная синхронизация     │
│ ✗ Vector (устарел)                                       │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ Много чтения И запись примерно одинаково                 │
├──────────────────────────────────────────────────────────┤
│ ✓ Collections.synchronizedList + явная синхронизация     │
│ ✓ ArrayList + ReentrantReadWriteLock                     │
│ ✗ Vector (неэффективен)                                  │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ Частые compound operations                               │
├──────────────────────────────────────────────────────────┤
│ ✓ Collections.synchronizedList + явная синхронизация     │
│ ✓ ConcurrentHashMap (для map)                            │
│ ✗ Vector (false sense of safety)                         │
└──────────────────────────────────────────────────────────┘

Практический пример: правильная реализация

public class ThreadSafeCounter {
    // Неправильно:
    Vector<Integer> badWay = new Vector<>();
    
    // Правильно:
    private final List<Integer> list;
    private final Object lock = new Object();
    
    public ThreadSafeCounter() {
        this.list = new ArrayList<>();
    }
    
    public void addIfNotExists(Integer value) {
        synchronized (lock) {
            if (!list.contains(value)) {
                list.add(value);
            }
        }
    }
    
    public int getCount() {
        synchronized (lock) {
            return list.size();
        }
    }
    
    public List<Integer> getAllValues() {
        synchronized (lock) {
            return new ArrayList<>(list); // Copy для безопасности
        }
    }
}

Заключение

Vector ЯВЛЯЕТСЯ потокобезопасным на уровне отдельных методов, но это приводит к:

  1. False sense of security — разработчик думает что код потокобезопасен, а это не так
  2. Race condition на уровне операций — несколько синхронизированных методов не гарантируют безопасность compound operation
  3. Производительность — синхронизация всех методов неэффективна
  4. Устаревание — Vector — это legacy код из Java 1.0

Правильный подход:

  • Не использовать Vector (он deprecated для многопоточности)
  • Использовать ArrayList + явная синхронизация для compound operations
  • Использовать CopyOnWriteArrayList если много чтения и мало записи
  • Использовать Collections.synchronizedList только если вы понимаете его ограничения
  • Для сложных сценариев использовать ConcurrentHashMap или собственные блокировки