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

Почему String является потокобезопасным?

1.8 Middle🔥 161 комментариев
#Многопоточность#Основы Java

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

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

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

Почему String является потокобезопасным в Java

Краткий ответ

String потокобезопасен, потому что он неизменяем (immutable). Раз объект не может изменяться, многие потоки могут его читать одновременно без синхронизации.

Что значит immutable (неизменяемый)

String str = "Hello";
str = str.toUpperCase();  // ← Это НЕ изменяет исходный объект!
// А создаёт НОВЫЙ объект "HELLO"

System.out.println(str);  // HELLO
System.out.println("Hello");  // Исходная строка не изменилась

Структура String в Java

public final class String implements Serializable, Comparable<String>, CharSequence {
    // Основные поля
    private final char[] value;      // ← final: не может быть переназначен
    private final int hash;          // ← final: кэшированный hash
    private final int offset;        // ← Используется в некоторых версиях Java
    private final int count;         // ← Используется в некоторых версиях Java
    
    // Конструктор
    public String(char[] value) {
        this.value = Arrays.copyOf(value, value.length);  // Копирование!
    }
    
    // Методы НЕ изменяют исходную строку
    public String toUpperCase() {
        // Возвращает НОВЫЙ объект String
    }
    
    public String concat(String str) {
        // Возвращает НОВЫЙ объект String
    }
    
    public String substring(int beginIndex) {
        // Возвращает НОВЫЙ объект String
    }
}

Ключевые особенности:

  • final class — нельзя наследовать
  • private final char[] — поле приватное и final
  • Все методы возвращают новые объекты — исходный не меняется

Почему immutable = потокобезопасный

Если объект не может меняться, нет race conditions:

// Безопасно без синхронизации
String shared = "Java";

// Поток 1
Thread t1 = new Thread(() -> {
    System.out.println(shared);  // Java
    System.out.println(shared.length());  // 4
});

// Поток 2
Thread t2 = new Thread(() -> {
    System.out.println(shared);  // Java (всегда то же значение)
    System.out.println(shared.length());  // 4
});

t1.start();
t2.start();
// Оба потока видят одно и то же значение "Java"
// Никаких race conditions!

Сравнение: изменяемый vs неизменяемый

Изменяемый класс (ОПАСЕН в многопоточности):

public class MutableUser {  // ← Не final
    private String name;    // ← Не final
    
    public void setName(String name) {
        this.name = name;  // ← Изменяет объект
    }
    
    public String getName() {
        return name;
    }
}

// Потокобезопасность нарушена!
MutableUser user = new MutableUser();
user.setName("Alice");

Thread t1 = new Thread(() -> {
    String name = user.getName();
    // ← Может быть null, если другой поток вызвал setName(null)
});

Thread t2 = new Thread(() -> {
    user.setName(null);  // ← Другой поток меняет значение
});

Неизменяемый класс (БЕЗОПАСЕН):

public final class ImmutableUser {  // ← final
    private final String name;      // ← final
    
    public ImmutableUser(String name) {
        this.name = name;  // Назначение только в конструкторе
    }
    
    public String getName() {
        return name;  // Только чтение, без setter'ов
    }
    
    // Методы возвращают ДЕМОЙные объекты
    public ImmutableUser withName(String newName) {
        return new ImmutableUser(newName);  // ← Создаём новый объект
    }
}

// Потокобезопасно!
ImmutableUser user = new ImmutableUser("Alice");

Thread t1 = new Thread(() -> {
    String name = user.getName();  // ← Всегда "Alice", безопасно
});

Thread t2 = new Thread(() -> {
    ImmutableUser newUser = user.withName("Bob");  // ← Создаёт новый объект
    // Исходный user остался "Alice"
});

Как String защищает свои данные

1. Копирование при создании:

char[] chars = {'H', 'e', 'l', 'l', 'o'};
String str = new String(chars);

chars[0] = 'J';  // Меняем исходный массив

System.out.println(str);  // "Hello" - не изменилась!
// Потому что String сделал копию в конструкторе

2. Нет setter'ов:

public class String {
    private final char[] value;
    
    // Нет методов setCharAt(), setValue() и т.д.
    // Нет способа изменить содержимое
}

3. Кэшированный hash код:

public class String {
    private final int hash;  // ← Вычисляется один раз
    
    @Override
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            h = hash(value);
            hash = h;  // Кэш
        }
        return h;
    }
}

// Потому что строка не меняется, hash остаётся тем же
// Это оптимизация для HashMap/HashSet

Пример потокобезопасности на практике

public class ThreadSafetyDemo {
    // Общая строка для двух потоков
    static String shared = "I Love Java";
    
    public static void main(String[] args) throws InterruptedException {
        // Поток 1: читает и обрабатывает
        Thread reader1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                String upperCase = shared.toUpperCase();
                int length = shared.length();
                System.out.println("Reader 1: " + upperCase + ", length: " + length);
                // Безопасно! shared не меняется
            }
        });
        
        // Поток 2: читает и обрабатывает
        Thread reader2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                String lowerCase = shared.toLowerCase();
                int length = shared.length();
                System.out.println("Reader 2: " + lowerCase + ", length: " + length);
                // Безопасно! shared не меняется
            }
        });
        
        reader1.start();
        reader2.start();
        
        reader1.join();
        reader2.join();
        
        System.out.println("Всё прошло без ошибок и race conditions!");
    }
}

Сравнение с мутабельным StringBuffer/StringBuilder

// StringBuffer - СИНХРОНИЗИРОВАН (потокобезопасен, но медленен)
public synchronized StringBuffer append(String str) {
    // Медленнее, потому что синхронизирован
}

// StringBuilder - НЕ синхронизирован (быстрее, но не потокобезопасен)
public StringBuilder append(String str) {
    // Быстрее, но используйте только в одном потоке
}

// String - неизменяем (потокобезопасен, не синхронизирован)
public final class String {
    // Потокобезопасен без синхронизации
}
КлассПотокобезопасенМутабельностьСкорость
StringДаНетБыстро (кэширование)
StringBufferДаДаМедленно (синхронизация)
StringBuilderНетДаОчень быстро

Правила создания неизменяемого класса

Если хотите создать свой immutable класс:

public final class ImmutablePoint {  // 1. final class
    private final int x;              // 2. final fields
    private final int y;              // 2. final fields
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() { return x; }   // 3. Только getter'ы
    public int getY() { return y; }   // 3. Только getter'ы
    
    // 4. Возвращаем новые объекты
    public ImmutablePoint move(int dx, int dy) {
        return new ImmutablePoint(x + dx, y + dy);
    }
    
    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }
}

Заключение

String потокобезопасен, потому что:

Immutable — никогда не меняется ✅ final class — нельзя переопределить поведение ✅ final fields — приватные, не переназначаются ✅ Копирование — входные данные копируются ✅ Без setter'ов — нельзя изменить ✅ Все операции возвращают новые объекты — исходный не трогается

Поэтому миллиарды потоков могут одновременно читать один объект String без каких-либо синхронизаций. Это мощное проектировочное решение!