Если вы объявляете поле final
, это нечто большее, чем сделать его ошибкой во время компиляции, чтобы попытаться изменить поле или оставить его неинициализированным.
В многопоточном коде, если вы совместно используете экземплярываш класс A
с гонками данных (то есть без какой-либо синхронизации, т. е. путем сохранения его в глобально доступном месте, например в статическом поле), возможно, что некоторые потоки увидят значение getA()
change!
Final
гарантируется (в JVM specs ), что его значения будут видны всем потокам после завершения конструктора, даже без синхронизации.
Рассмотрим эти два класса:
final class A {
private final int x;
A(int x) { this.x = x; }
public getX() { return x; }
}
final class B {
private int x;
B(int x) { this.x = x; }
public getX() { return x; }
}
Оба A
и B
являются неизменяемыми, в том смысле, что вы не можете изменить значение поля x
после инициализации (давайте забудем об отражении).Единственное отличие состоит в том, что поле x
помечено final
в A. Вы скоро поймете огромное значение этой крошечной разницы.
Теперь рассмотрим следующий код:
class Main {
static A a = null;
static B b = null;
public static void main(String[] args) {
new Thread(new Runnable() { void run() { try {
while (a == null) Thread.sleep(50);
System.out.println(a.getX()); } catch (Throwable t) {}
}}).start()
new Thread(new Runnable() { void run() { try {
while (b == null) Thread.sleep(50);
System.out.println(b.getX()); } catch (Throwable t) {}
}}).start()
a = new A(1); b = new B(1);
}
}
Предположим, что оба потока видят, что поля, которые они наблюдают, не равны NULL после того, как их установил основной поток (обратите внимание, что, хотя это предположение может выглядеть тривиальным, JVM не гарантирует этого!).
В этом случае мы можем быть уверены, что поток, который наблюдает за a
, напечатает значение 1
, потому что его поле x
является окончательным - так что, после завершения конструктора, гарантируется, что все потоки, которые видятобъект увидит правильные значения для x
.
Однако мы не можем быть уверены в том, что будет делать другой поток.Спецификации могут гарантировать только то, что он напечатает либо 0
, либо 1
.Так как поле не final
, и мы не использовали никакой синхронизации (synchronized
или volatile
), поток может увидеть поле неинициализированным и вывести 0!Другая возможность состоит в том, что он фактически видит инициализированное поле и печатает 1. Он не может печатать никакие другие значения.
Кроме того, может произойти следующее: если вы продолжите читать и печатать значение getX()
изb
, он может начать печатать 1
через некоторое время после печати 0
!В этом случае ясно, почему неизменяемые объекты должны иметь свои поля final
: с точки зрения второго потока, b
изменился, даже если предполагается, что он неизменен, не предоставляя сеттеры!
Если вы хотите гарантировать, что второй поток увидит правильное значение для x
, не делая поле final
, вы можете объявить поле, содержащее экземпляр B
volatile:
class Main {
// ...
volatile static B b;
// ...
}
Другая возможность заключается в синхронизации при настройке и при чтении поля, либо путем изменения класса B:
final class B {
private int x;
private synchronized setX(int x) { this.x = x; }
public synchronized getX() { return x; }
B(int x) { setX(x); }
}
, либо путем изменения кода Main, добавляя синхронизацию к полю b
прочитано, а когда написано - обратите внимание, что обе операции должны синхронизироваться на одном и том же объекте!
Как видите, самое элегантное, надежное и производительное решение - сделать поле x
final.
В заключение: для неизменяемых, поточно-ориентированных классов не обязательно, чтобы все их поля были окончательными .Однако эти классы (поточно-ориентированные, неизменяемые, содержащие неконечные поля) должны разрабатываться с особой тщательностью и должны быть оставлены для экспертов.
Примером этого является класс java.lang.String.Он имеет поле private int hash;
, которое не является окончательным, и используется в качестве кэша для hashCode ():
private int hash;
public int hashCode() {
int h = hash;
int len = count;
if (h == 0 && len > 0) {
int off = offset;
char val[] = value;
for (int i = 0; i < len; i++)
h = 31*h + val[off++];
hash = h;
}
return h;
}
Как видите, метод hashCode () сначала читает (непоследнее) поле hash
.Если он неинициализирован (то есть если он равен 0), он пересчитает свое значение и установит его.Для потока, который вычислил хеш-код и записал в поле, он сохранит это значение навсегда.
Однако другие потоки могут все еще видеть 0 для поля, даже после того, как поток установил его для чего-то еще.В этом случае эти другие потоки пересчитают хэш и получат точно такое же значение , а затем установят его.
Здесь, что оправдывает неизменность и безопасность потока класса, так это то, что каждый поток получит точно такое же значение для hashCode (), даже если он будет кэширован в неконечном поле, потому что он будет пересчитан ибудет получено точно такое же значение.
Все эти рассуждения очень тонкие, и поэтому рекомендуется, чтобы все поля были помечены как окончательные в неизменяемых, поточно-ориентированных классах .