Synchronisation de constructeur en Java

Quelqu’un, quelque part, m’a dit que les constructeurs Java étaient synchronisés pour qu’il ne soit pas accessible simultanément pendant la construction, et je me demandais si un constructeur stocke l’object dans une carte et qu’un autre thread le récupère avant sa construction. est terminé, est-ce que ce thread bloquera jusqu’à ce que le constructeur termine?

Permettez-moi de démontrer avec un code:

public class Test { private static final Map testsById = Collections.synchronizedMap(new HashMap()); private static final AtomicInteger atomicIdGenerator = new AtomicInteger(); private final int id; public Test() { this.id = atomicIdGenerator.getAndIncrement(); testsById.put(this.id, this); // Some lengthy operation to fully initialize this object } public static Test getTestById(int id) { return testsById.get(id); } } 

Supposons que put / get soit la seule opération sur la map, donc je n’obtiendrai pas de CME par quelque chose comme une itération, et j’essaierai d’ignorer d’autres défauts évidents ici.

Ce que je veux savoir, c’est si un autre thread (ce n’est pas celui qui construit l’object, évidemment) essaie d’accéder à l’object en utilisant getTestById et d’appeler quelque chose dessus, va-t-il bloquer? En d’autres termes:

 Test test = getTestById(someId); test.doSomething(); // Does this line block until the constructor is done? 

J’essaie juste de clarifier jusqu’où va la synchronisation du constructeur en Java et si un tel code serait problématique. J’ai vu un code comme celui-ci récemment qui l’a fait au lieu d’utiliser une méthode d’usine statique, et je me demandais à quel point c’est dangereux (ou sûr) dans un système multi-thread.

Quelqu’un, quelque part, m’a dit que les constructeurs Java étaient synchronisés afin de ne pas pouvoir y accéder simultanément pendant la construction

Ce n’est certainement pas le cas. Il n’y a pas de synchronisation implicite avec les constructeurs. Plusieurs constructeurs peuvent non seulement arriver en même temps, mais vous pouvez également obtenir des problèmes de concurrence, par exemple en forçant un thread à l’intérieur d’un constructeur avec une référence à l’object en cours de construction.

Si j’ai un constructeur qui stocke l’object dans une carte et qu’un autre thread le récupère à partir de cette carte avant la fin de sa construction, est-ce que ce thread sera bloqué jusqu’à ce que le constructeur soit complet?

Non ça ne va pas.

Le gros problème avec les constructeurs dans les applications threadées est que le compilateur a la permission, sous le modèle de mémoire Java, de réorganiser les opérations à l’intérieur du constructeur pour qu’elles aient lieu après la création de la référence d’object et la fin du constructeur. final champs final seront garantis pour être complètement initialisés au moment où le constructeur aura terminé, mais pas les autres champs “normaux”.

Dans votre cas, puisque vous mettez votre Test dans la carte synchronisée et continuez à effectuer l’initialisation, comme @Tim mentionné, cela permettra à d’autres threads de récupérer l’object dans un état éventuellement semi-initialisé. Une solution consisterait à utiliser une méthode static pour créer votre object:

 private Test() { this.id = atomicIdGenerator.getAndIncrement(); // Some lengthy operation to fully initialize this object } public static Test createTest() { Test test = new Test(); // this put to a synchronized map will force the happens-before of the Test constructor testsById.put(test.id, test); return test; } 

Mon exemple de code fonctionne puisque vous avez affaire à une carte synchronisée, qui effectue un appel à la synchronized qui garantit que le constructeur de Test est terminé et a été synchronisé en mémoire.

Les gros problèmes dans votre exemple sont à la fois la garantie “arrive avant” (le constructeur peut ne pas se terminer avant que Test soit placé dans la carte) et la synchronisation de la mémoire (le thread de construction et le thread de lecture peuvent voir la mémoire différente pour l’instance de Test ) . Si vous déplacez le put dehors du constructeur, les deux sont gérés par la carte synchronisée. Quel que soit l’object sur lequel la synchronized est effectuée, le fait que le constructeur ait été terminé avant d’être placé dans la carte et que la mémoire a été synchronisée sont garantis.

Je crois que si vous avez appelé testsById.put(this.id, this); à la toute fin de votre constructeur, vous pourrez peut-être bien vous en servir, mais ce n’est pas une bonne forme et vous aurez au moins besoin de commentaires / de la documentation minutieux. Cela ne résoudrait pas le problème si la classe était sous-classée et que l’initialisation était effectuée dans la sous-classe après le super() . La solution static j’ai montrée est un meilleur modèle.

Vous avez été mal informé. Ce que vous décrivez est en fait appelé publication inappropriée et discuté en détail dans le livre Java Concurrency In Practice.

Donc oui, il sera possible pour un autre thread d’obtenir une référence à votre object et de commencer à essayer de l’utiliser avant la fin de l’initialisation. Mais attendez, il y a pire: considérez cette réponse: https://stackoverflow.com/a/2624784/122207 … en gros, il peut y avoir une réorganisation de l’atsortingbution de référence et de l’achèvement du constructeur. Dans l’exemple référencé, un thread peut affecter h = new Holder(i) et un autre thread appel h.assertSanity() sur la nouvelle instance avec la synchronisation juste pour obtenir deux valeurs différentes pour le membre n assigné dans le constructeur de Holder . .

Quelqu’un quelque part m’a dit que les constructeurs Java sont synchronisés

“Quelqu’un quelque part” est sérieusement mal informé. Les constructeurs ne sont pas synchronisés. Preuve:

 public class A { public A() throws InterruptedException { wait(); } public static void main(Ssortingng[] args) throws Exception { A a = new A(); } } 

Ce code déclenche une java.lang.IllegalMonitorStateException à l’appel wait() . Si la synchronisation était effective, ce ne serait pas le cas.

Cela n’a même pas de sens. Il n’y a pas besoin de les synchroniser. Un constructeur ne peut être appelé qu’après un new(), et, par définition, chaque appel de new() renvoie une valeur différente. Il n’y a donc aucune possibilité qu’un constructeur soit appelé par deux threads simultanément avec la même valeur. Il n’y a donc pas besoin de synchronisation des constructeurs.

Si j’ai un constructeur qui stocke l’object dans une carte et qu’un autre thread le récupère à partir de cette carte avant la fin de sa construction, est-ce que ce thread sera bloqué jusqu’à ce que le constructeur soit complet?

Pourquoi ferait-il cela? Qui va le bloquer? Laisser “ceci” s’échapper d’un constructeur comme celui-là est une mauvaise pratique: cela permet à d’autres threads d’accéder à un object en construction.

les constructeurs sont comme les autres méthodes, il n’y a pas de synchronisation supplémentaire (sauf pour le traitement des champs final ).

le code fonctionnerait si this est publié plus tard

 public Test() { // Some lengthy operation to fully initialize this object this.id = atomicIdGenerator.getAndIncrement(); testsById.put(this.id, this); } 

Bien que cette question soit résolue mais que le code collé en question ne suive pas les techniques de construction sûres car elle permet à cette référence de s’échapper du constructeur , j’aimerais partager une belle explication présentée par Brian Goetz dans l’ article: “Théorie et pratique Java: Techniques de construction sécurisées “sur le site Web IBM developerWorks .

C’est dangereux Il n’y a pas de synchronisation supplémentaire dans JVM. Vous pouvez faire quelque chose comme ça:

 public class Test { private final Object lock = new Object(); public Test() { synchronized (lock) { // your improper object reference publication // long initialization } } public void doSomething() { synchronized (lock) { // do something } } }