Maison >Java >javaDidacticiel >Implémentation partagée d'objets dans la programmation simultanée Java
Normalement, nous ne pouvons pas garantir que le thread effectuant l'opération de lecture puisse voir les valeurs écrites par d'autres threads, car chaque thread a son propre mécanisme de mise en cache. Pour garantir la visibilité des opérations d'écriture mémoire entre plusieurs threads, un mécanisme de synchronisation doit être utilisé.
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
Le code ci-dessus semble afficher 42, mais en fait, il peut ne pas se terminer du tout, car le thread de lecture ne peut jamais voir la valeur de prêt, il est très probable qu'il génère 0, car le thread de lecture voit la valeur de prêt ; écrit, mais la valeur écrite dans le numéro n'est pas visible par la suite. Ce phénomène est appelé « réorganisation ». En l'absence de synchronisation, le compilateur, le processeur, le runtime, etc. peuvent apporter des ajustements inattendus à l'ordre d'exécution des opérations.
Ainsi, chaque fois que des données sont partagées entre plusieurs threads, vous devez utiliser une synchronisation appropriée.
À moins que la synchronisation ne soit utilisée, il est très probable qu'elle obtienne la valeur invalide de la variable. Les valeurs invalides peuvent ne pas apparaître en même temps et un thread peut obtenir la dernière valeur d'une variable et la valeur invalide d'une autre variable. Des données invalides peuvent également entraîner des échecs déroutants, tels que des exceptions inattendues, des structures de données corrompues, des calculs inexacts, des boucles infinies, etc.
Pour les variables longues et doubles non volatiles, la JVM permet de décomposer une opération de lecture ou d'écriture 64 bits en deux opérations 32 bits. Par conséquent, il est très probable que les 32 bits supérieurs de la dernière valeur et les 32 bits inférieurs de la valeur invalide soient lus, ce qui entraînerait la lecture d'une valeur aléatoire. Sauf s’ils sont déclarés avec le mot-clé volatile ou protégés par un verrou.
Lorsqu'un thread exécute un bloc de code synchronisé protégé par un verrou, il peut voir les résultats de toutes les opérations précédentes des autres threads dans le même bloc de code synchronisé. Sans synchronisation, les garanties ci-dessus ne peuvent être obtenues. La signification du verrouillage ne se limite pas au comportement d'exclusion mutuelle, mais inclut également la visibilité. Pour garantir que tous les threads voient la dernière valeur d'une variable partagée, tous les threads effectuant des opérations de lecture ou d'écriture doivent être synchronisés sur le même verrou.
Lorsqu'une variable est déclarée comme type volatile, ni le compilateur ni le runtime ne réorganiseront les opérations sur la variable avec les autres opérations de mémoire. Les variables volatiles ne sont pas mises en cache dans des registres ou à d'autres endroits invisibles pour le processeur, donc la lecture d'une variable volatile renvoie toujours la valeur écrite la plus récemment. Le mécanisme de verrouillage peut garantir à la fois la visibilité et l'atomicité, tandis que les variables volatiles ne peuvent assurer que la visibilité.
Les variables volatiles doivent être utilisées si et seulement si toutes les conditions suivantes sont remplies :
L'opération d'écriture dans la variable ne dépend pas de la valeur actuelle de la variable, ou il est assuré qu'un seul thread est utilisé pour mettre à jour la valeur de la variable.
Cette variable ne sera pas incluse dans la condition d'invariance avec d'autres variables d'état.
Pas besoin de verrouiller lors de l'accès aux variables.
Publier un objet signifie que l'objet peut être utilisé dans le code en dehors de la portée actuelle. Les méthodes de publication d'objets incluent : les références à des variables non privées, les références renvoyées par les appels de méthode, la publication d'objets de classe interne et les références implicites à des classes externes, etc. Lorsqu’un objet est libéré alors qu’il ne devrait pas l’être, on parle de fuite.
public class ThisEscape { private int status; public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); status = 1; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
Étant donné que les instances de classes internes contiennent des références implicites à des instances de classes externes, lorsque ThisEscape publie EventListener, il publie également implicitement l'instance ThisEscape elle-même. Mais pour le moment, le statut de la variable n'a pas été initialisé, ce qui a provoqué une fuite de cette référence dans le constructeur. Vous pouvez utiliser un constructeur privé et une méthode de fabrique publique pour éviter des processus de construction incorrects :
public class SafeListener { private int status; private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; status = 1; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
Une façon d'éviter d'utiliser la synchronisation est de ne pas partager. Si les données ne sont accessibles que dans un seul thread, aucune synchronisation n'est requise, appelée fermeture de thread. Le confinement des threads est une considération de programmation et doit être implémenté dans le programme. Java fournit également certains mécanismes pour aider à maintenir la fermeture des threads, tels que les variables locales et ThreadLocal.
La fermeture de fil ad hoc signifie que la responsabilité du maintien de la fermeture du fil est entièrement supportée par la mise en œuvre du programme. L'utilisation de variables volatiles est un moyen d'obtenir une fermeture de thread ad hoc. Tant qu'il est garanti qu'un seul thread effectue des opérations d'écriture sur des variables volatiles partagées, il est alors sûr d'effectuer des opérations de « lecture-modification-écriture » sur ces variables. , la visibilité des variables volatiles garantit que les autres threads peuvent voir la dernière valeur.
La fermeture de fil ad hoc est très fragile, utilisez-la donc le moins possible dans votre programme. Dans la mesure du possible, utilisez d’autres techniques de confinement de thread telles que le confinement de pile et ThreadLocal.
Dans la fermeture de pile, les objets ne sont accessibles que via des variables locales. Ils sont situés sur la pile du thread d’exécution et ne sont pas accessibles aux autres threads. Même si ces objets ne sont pas thread-safe, ils le sont toujours. Cependant, il convient de noter que seule la personne qui écrit le code sait quels objets sont inclus dans la pile. Sans instructions claires, les responsables ultérieurs peuvent facilement divulguer ces objets par erreur.
使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:
public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); } }
从概念上看,你可以将ThreadLocal8742468051c85b06f0a0af9e3e506b5c视为包含了Mapdd13f7ef6939263b34f16ddd764e4ff9对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。
如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:
对象创建以后其状态就不能改变
对象的所有域都是final类型
对象是正确创建的,在对象创建期间,this引用没有泄露
public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } }
上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。
final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。
因式分解Sevlet将执行两个原子操作:
更新缓存
通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果
每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:
public class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } }
当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:
public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } }
像这样将对象引用保存到公有域中就是不安全的:
public Holder holder; public void initialize(){ holder = new Holder(42); }
由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); } }
上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。
即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:
在静态初始化函数里初始化一个对象引用。
将对象的引用保存到volatile类型的域或者AtomicReference对象中。
将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。
线程安全库中的容器类提供了以下的安全发布保证:
通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。
通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。
En plaçant un objet dans une BlockingQueue ou une ConcurrentLinkedQueue, vous pouvez publier l'objet en toute sécurité sur n'importe quel thread qui accède à l'objet à partir de ces files d'attente.
Si un objet est techniquement mutable, mais que son état ne changera pas après sa libération, alors un tel objet est appelé un objet immuable de facto. Un objet immuable de facto publié en toute sécurité peut être utilisé en toute sécurité par n'importe quel thread sans synchronisation supplémentaire. Par exemple, gérez un objet Map qui stocke la dernière heure de connexion de chaque utilisateur :
public Map7c36a245a97922f78d3224cbec5818e1 lastLogin =
Collections.synchronizedMap(new HashMap
Pour les objets mutables, la synchronisation est non seulement nécessaire lors de la publication de l'objet, mais doit également être utilisée à chaque accès à l'objet pour garantir la visibilité des opérations de modification ultérieures. Les exigences de publication d'un objet dépendent de sa mutabilité :
Les objets immuables peuvent être publiés par n'importe quel mécanisme.
Faits Les objets immuables doivent être publiés de manière sécurisée.
Les objets mutables doivent être libérés de manière sûre et doivent être thread-safe ou protégés par un verrou.
Il existe des stratégies pratiques que vous pouvez utiliser lors de l'utilisation et du partage d'objets dans des programmes simultanés, notamment :
Thread Confinement. Un objet inclus dans un thread ne peut appartenir qu'à un seul thread, l'objet est inclus dans ce thread et ne peut être modifié que par ce thread.
Partage en lecture seule. Sans synchronisation supplémentaire, un objet partagé en lecture seule est accessible simultanément par plusieurs threads, mais aucun thread ne peut le modifier. Les objets partagés en lecture seule incluent les objets immuables et les objets immuables de facto.
Partage sécurisé. Un objet thread-safe est synchronisé en interne afin que plusieurs threads puissent y accéder via l'interface publique de l'objet sans autre synchronisation.
Protéger l'objet. Les objets protégés ne sont accessibles qu'en détenant un verrou spécifique. Les objets protégés incluent les objets encapsulés dans d'autres objets thread-safe, ainsi que les objets libérés et protégés par un verrou spécifique.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!