Selon les spécifications de langage Java , les constructeurs ne peuvent pas être marqués comme synchronisés car les autres threads ne peuvent pas voir l'objet en cours de création tant que le thread qui l'a créé ne l'a pas terminé. Cela semble un peu étrange, car je peux effectivement avoir un autre fil de vue sur l'objet pendant sa construction:
public class Test {
public Test() {
final Test me = this;
new Thread() {
@Override
public void run() {
// ... Reference 'me,' the object being constructed
}
}.start();
}
}
Je sais que c’est un exemple assez artificiel, mais il semble théoriquement que l’on puisse imaginer un cas plus réaliste dans lequel marquer le constructeur synchronisé serait légitime pour éviter des courses avec des threads comme celui-ci.
Ma question est la suivante: existe-t-il une raison pour laquelle Java interdirait spécifiquement le modificateur synchronisé sur un constructeur? Peut-être que mon exemple ci-dessus est imparfait, ou peut-être qu'il n'y a vraiment aucune raison et que c'est une décision arbitraire. Dans les deux cas, je suis vraiment curieux et j'aimerais connaître la réponse.
Si vous avez vraiment besoin de synchroniser le reste du constructeur par rapport aux threads qui obtiennent de toute façon une référence à votre objet pas encore totalement construit, vous pouvez utiliser un bloc synchronisé:
public class Test {
public Test() {
final Test me = this;
synchronized(this) {
new Thread() {
@Override
public void run() {
// ... Reference 'me,' the object being constructed
synchronized(me) {
// do something dangerous with 'me'.
}
}
}.start();
// do something dangerous with this
}
}
}
Habituellement, il est considéré comme un mauvais style de "donner" votre objet non encore construit comme ceci, de sorte qu'un constructeur synchronisé n'est pas nécessaire.
Dans certains cas, un constructeur synchronisé serait utile. Voici un exemple plus réaliste, tiré de la discussion de la réponse de Bozho:
public abstract class SuperClass {
public SuperClass() {
new Thread("evil") { public void run() {
doSomethingDangerous();
}}).start();
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) { /* ignore */ }
}
public abstract void doSomethingDangerous();
}
public class SubClass extends SuperClass {
int number;
public SubClass () {
super();
number = 2;
}
public synchronized void doSomethingDangerous() {
if(number == 2) {
System.out.println("everything OK");
}
else {
System.out.println("we have a problem.");
}
}
}
Nous voulons que la méthode doSomethingDangerous()
ne soit appelée qu'une fois la construction de notre objet SubClass terminée, par exemple. nous voulons seulement la sortie "tout va bien". Mais dans ce cas, lorsque vous ne pouvez éditer que votre sous-classe, vous n’avez aucune chance de le faire. Si le constructeur pouvait être synchronisé, le problème serait résolu.
Donc, ce que nous apprenons à ce sujet: ne faites jamais quelque chose comme ce que j'ai fait ici dans le constructeur de la super-classe, si votre classe n'est pas finale - et n'appelez aucune méthode non-finale de votre propre classe à partir de votre constructeur.
La question a été soulevée sur une liste de discussion utilisée par les rédacteurs de l'API simultanée Java et du modèle de mémoire Java. Plusieurs réponses ont été données, en particulier Hans Boehm a répondu :
Certains d’entre nous (dont moi-même l’IIRC) ont affirmé lors des délibérations sur le modèle de mémoire Java que les constructeurs synchronisés devraient être autorisés. Maintenant, je pourrais aller dans un sens ou dans l’autre. Le code client ne doit pas utiliser de race pour communiquer la référence, cela n'a donc aucune importance. Mais si vous ne faites pas confiance aux clients de [votre classe], je pense que les constructeurs synchronisés pourraient éventuellement être utiles. Et c’était en grande partie le raisonnement derrière la sémantique finale sur le terrain. [...] Comme David l'a dit, vous pouvez utiliser des blocs synchronisés.
Parce que synchronized
garantit que les actions sur les mêmes objets ne doivent pas être effectuées par plusieurs threads. Et quand le constructeur s'appelle, vous n'avez toujours pas l'objet. Il est logiquement impossible pour deux threads d'accéder au constructeur du même objet.
Dans votre exemple, même si une méthode est invoquée par le nouveau thread, il ne s'agit plus du constructeur, mais de la méthode cible en cours de synchronisation ou non.
Dans votre exemple, le constructeur n'est appelé en réalité qu'une seule fois à partir d'un thread.
Oui, il est possible d’obtenir une référence à un objet incomplètement construit (certaines discussions sur le verrouillage à double contrôle et la raison de sa rupture révèlent ce problème), mais pas en appelant le constructeur une seconde fois.
Syncronisé sur le constructeur empêcherait deux threads d'appeler le constructeur sur le même objet simultanément, ce qui n'est pas possible car il n'est jamais possible d'appeler le constructeur sur une instance d'objet deux fois, point.
La section Modificateurs de constructeur dans JLS indique clairement
There is no practical need for a constructor to be synchronized, because it would
lock the object under construction, which is normally not made available to other
threads until all constructors for the object have completed their work.
Il n'est donc pas nécessaire que le constructeur soit synchronisé.
De plus, il n'est pas recommandé de donner la référence aux objets (this) avant la création de l'objet. Une des situations ambiguës possibles serait de donner la référence aux objets. Le constructeur de la superclasse lors de la création de l’objet sous-classe.
Je vois peu de raisons d'interdire aux constructeurs d'être synchronisés. Cela serait utile dans de nombreux scénarios d'applications multithreads. Si je comprends correctement le modèle de mémoire Java (j'ai lu http://jeremymanson.blogspot.se/2008/11/what-volatile-means-in-Java.html ), la classe simple suivante aurait pu bénéficier d'un constructeur synchronisé.
public class A {
private int myInt;
public /*synchronized*/ A() {
myInt = 3;
}
public synchronized void print() {
System.out.println(myInt);
}
}
En théorie, je pense qu'un appel à print()
pourrait imprimer "0". Cela peut se produire si une instance de A est créée par le thread 1, si la référence à l'instance est partagée avec le thread 2 et si le thread 2 appelle print()
. S'il n'y a pas de synchronisation spéciale entre l'écriture myInt = 3
du thread 1 et la lecture du même champ par le thread 2, il n'est pas garanti que le thread 2 voie l'écriture.
Un constructeur synchronisé résoudrait ce problème. Ai-je raison à ce sujet?
Notez que les constructeurs ne peuvent pas être synchronisés. L'utilisation du mot-clé synchronized avec un constructeur est une erreur de syntaxe. Synchroniser les constructeurs n'a pas de sens, car seul le thread qui crée un objet doit y avoir accès pendant sa construction.
Une telle synchronisation peut avoir un sens dans de très rares cas, mais je suppose que cela ne vaut tout simplement pas la peine:
En cas de doute, laissez tomber.
Le code suivant peut atteindre le résultat attendu pour le constructeur synchronisé.
public class SynchronisedConstructor implements Runnable {
private int myInt;
/*synchronized*/ static {
System.out.println("Within static block");
}
public SynchronisedConstructor(){
super();
synchronized(this){
System.out.println("Within sync block in constructor");
myInt = 3;
}
}
@Override
public void run() {
print();
}
public synchronized void print() {
System.out.println(Thread.currentThread().getName());
System.out.println(myInt);
}
public static void main(String[] args) {
SynchronisedConstructor sc = new SynchronisedConstructor();
Thread t1 = new Thread(sc);
t1.setName("t1");
Thread t2 = new Thread(sc);
t2.setName("t2");
t1.start();
t2.start();
}
}