Ответ 1
Это зависит от того, где вы делитесь ими:
- Небезопасно делиться ими внутри scala -library
- Небезопасно делиться ими с Java-кодом, отражением
Проще говоря, эти коллекции менее защищены, чем объекты с только конечными полями. Независимо от того, что они одинаковы на уровне JVM (без оптимизации, например ldc
) - оба могут быть полями с некоторым изменяемым адресом, поэтому вы можете изменить их с помощью команды putfield
bytecode. В любом случае, var
по-прежнему менее защищен компилятором в сравнении с java final
, scala final val
и val
.
Тем не менее, в большинстве случаев все равно использовать их, поскольку их поведение логически неизменено - все изменяемые операции инкапсулированы (для scala -code). Давайте посмотрим на Vector
. Для реализации алгоритма добавления требуется изменяемые поля:
private var dirty = false
//from VectorPointer
private[immutable] var depth: Int = _
private[immutable] var display0: Array[AnyRef] = _
private[immutable] var display1: Array[AnyRef] = _
private[immutable] var display2: Array[AnyRef] = _
private[immutable] var display3: Array[AnyRef] = _
private[immutable] var display4: Array[AnyRef] = _
private[immutable] var display5: Array[AnyRef] = _
который реализуется следующим образом:
val s = new Vector(startIndex, endIndex + 1, blockIndex)
s.initFrom(this) //uses displayN and depth
s.gotoPos(startIndex, startIndex ^ focus) //uses displayN
s.gotoPosWritable //uses dirty
...
s.dirty = dirty
И s
приходит к пользователю только после того, как метод вернул его. Таким образом, он даже не заботится о гарантиях happens-before
- все измененные операции выполняются в одном потоке (поток, где вы вызываете :+
, +:
или updated
)), это просто своего рода инициализация. Единственная проблема здесь в том, что private[somePackage]
доступен непосредственно из Java-кода и из scala -library, поэтому, если вы передадите его некоторые Java-методы могли бы изменить их.
Я не думаю, что вам следует беспокоиться о безопасности потоков, пусть скажет cons оператор. Он также имеет изменяемые поля:
final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
override def tail : List[B] = tl
override def isEmpty: Boolean = false
}
Но они использовали только внутри методов библиотеки (внутри одного потока) без какого-либо явного совместного использования или создания потоков, и они всегда возвращают новую коллекцию, рассмотрим take
в качестве примера:
override def take(n: Int): List[A] = if (isEmpty || n <= 0) Nil else {
val h = new ::(head, Nil)
var t = h
var rest = tail
var i = 1
while ({if (rest.isEmpty) return this; i < n}) {
i += 1
val nx = new ::(rest.head, Nil)
t.tl = nx //here is mutation of t filed
t = nx
rest = rest.tail
}
h
}
Итак, здесь t.tl = nx
не сильно отличается от t = nx
по смыслу безопасности потоков. Оба они отображаются только из одного стека (стек take
). Если я добавлю, скажем someActor ! t
(или любую другую операцию async), someField = t
или someFunctionWithExternalSideEffect(t)
прямо внутри цикла while
- я мог бы разорвать этот контракт.
Немного добавлено здесь об отношениях с JSR-133:
1) new ::(head, Nil)
создает новый объект в куче и помещает его адрес (допустим, 0x100500) в стек (val h =
)
2), пока этот адрес находится в стеке, он известен только текущему потоку
3) Другие потоки могут быть задействованы только после совместного использования этого адреса, помещая его в какое-то поле; в случае take
перед тем, как вызвать areturn
(return h
), он должен очистить любые кеши (для восстановления стека и регистров), поэтому возвращаемый объект будет согласован.
Таким образом, все операции над объектом 0x100500 выходят за рамки JSR-133, если 0x100500 является частью только стека (а не кучей, а не других стеков). Однако некоторые поля объекта 0x100500 могут указывать на некоторые общие объекты (которые могут быть в области JSR-133), но здесь это не так (поскольку эти объекты неизменяемы для внешних).
Я думаю, что (надежда) автор имел в виду логические гарантии синхронизации для разработчиков библиотек - вам все равно нужно быть осторожным с этими вещами, если вы разрабатываете scala -library, поскольку эти var
являются private[scala]
, private[immutable]
так что, возможно написать некоторый код, чтобы мутировать их из разных потоков. С точки зрения разработчика scala -library это обычно означает, что все мутации в одном экземпляре должны применяться в одном потоке и только в коллекции, невидимой для пользователя (на данный момент). Или просто говоря: не открывайте изменяемые поля для внешних пользователей.
P.S. scala имел несколько неожиданных проблем с синхронизацией, что вызвало непредсказуемость частей библиотеки, поэтому я бы не стал если что-то может быть неправильным (и это ошибка тогда), но, допустим, 99% случаев для 99% методов неизменяемых коллекций являются потокобезопасными. В худшем случае вам может быть отказано в использовании какого-либо сломанного метода или просто (это может быть не просто "просто" для некоторых случаев), нужно клонировать коллекцию для каждого потока.
В любом случае, неизменность по-прежнему является хорошим способом обеспечения безопасности потоков.
P.S.2 Экзотический случай, который может нарушить безопасность потоков неизменяемых коллекций, использует рефлексию для доступа к своим незавершенным полям.
Небольшое дополнение к другому экзотическому, но действительно ужасающему способу, как это было указано в комментариях к @Steve Waldman и @axel22 (автор). Если вы разделите неизменяемую коллекцию как часть некоторого объекта, совместно используемого между потоками && & если конструктор коллекции становится физически (по JIT) inlined (он по умолчанию не логически встроен) && & & если ваша JIT-реализация позволяет перестроить встроенный код с обычным - тогда вам нужно его синхронизировать (обычно этого достаточно, чтобы иметь @volatile
). Тем не менее, IMHO, я не верю, что последнее условие - правильное поведение, но пока не может ни доказать, ни опровергнуть это.