BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles 8 super nouveautés de Java 8 dont personne ne parle

8 super nouveautés de Java 8 dont personne ne parle

Favoris

Si vous n'avez pas vu de vidéos ou tutoriaux autour de Java 8, vous avez probablement été super occupés ou avez une vie sociale plus intéressante que la mienne (ce qui n'est pas si difficile). Avec des nouveautés telles que les expressions lambda et le Projet Nashorn qui attirent le plus les projecteurs, je voulais mettre l'accent sur de nouvelles APIs qui sont passées un petit peu sous le radar, mais qui rendent Java 8 meilleur de beaucoup de façons.

1. Stamped Locks (ou Verrous Estampillés)

Le code multi-threadé a longtemps été la plaie des développeurs serveurs (parlez-en juste avec l'Architecte Langage Java d'Oracle, et gourou de la concurrence, Brian Goetz). Au fil du temps, des idiomes complexes ont été ajoutés aux librairies du coeur de Java pour aider à minimiser les attentes de threads accédant à des ressources partagées. L'un d'entre eux est le classique ReadWriteLock qui vous permet de diviser le code en sections qui ont besoin d'être mutuellement exclusives (écritures), et d'autres qui n'en ont pas besoin (lectures).

Sur le papier, cela semble intéressant. Le problème est que le ReadWriteLock peut être très lent (jusqu'à 10x), ce qui va un peu à l'encontre de son but. Java 8 introduit un nouveau Verrou Lecture/Ecriture appelé StampedLock. La bonne nouvelle, c'est qu'il est furieusement rapide. La mauvaise nouvelle, c'est qu'il est plus compliqué à utiliser et embarque plus de données d'état. Il n'est également pas réentrant, ce qui signifie qu'un thread peut avoir le plaisir douteux de se deadlocker avec lui-même.

Le StampedLock a un mode "optimiste" qui diffuse un tampon retourné par chaque opération de verrouillage afin de servir en quelque sorte de ticket d'admission. Chaque opération de déverrouillage a besoin du tampon associé. Tout thread qui se trouve acquéreur d'un verrou en écriture alors qu'un lecteur possédait un verrou optimiste causera l'invalidation du déverrouillage optimiste (le tampon n'est plus valide). A ce moment, l'application peut recommencer de zéro, peut-être avec un verrou pessimiste (qui est aussi fourni par StampedLock). Gérer cela est à votre charge, et un tampon ne peut pas être utilisé pour en déverrouiller un autre, donc faites vraiment attention.

Voyons ce verrou en action :

long stamp = lock.tryOptimisticRead(); // non blocking path - super fast
work(); // we're hoping no writing will go on in the meanwhile
if (lock.validate(stamp)){
       //success! no contention with a writer thread 
}
else {
       //another thread must have acquired a write lock in the meanwhile, changing the stamp. 
       //bummer - let's downgrade to a heavier read lock

            stamp = lock.readLock(); //this is a traditional blocking read lock 
       try {
                 //no writing happening now
                 work();

       }
       finally {
            lock.unlock(stamp); // release using the correlating stamp
       }
}

2. Concurrent Adders

Une autre jolie nouveauté de Java 8, spécifiquement destinée au code s'exécutant à grande échelle, avec les "Adders" concurrents. L'un des patterns de concurrence les plus basiques est la lecture et l'écriture de la valeur d'un compteur numérique. En tant que tel, il existe aujourd'hui beaucoup de manières de réaliser cette opération, mais aucune aussi efficace et élégante que ce que Java 8 a à offrir.

Jusqu'à présent, cela était fait en utilisant les Atomics, qui utilisaient directement une instruction CPU de Comparer-Et-Echanger (Compare and Swap ou CAS, au travers de la classe sun.misc.Unsafe) pour tenter d'affecter la valeur d'un compteur. Le problème était que lorsqu'un CAS échouait à cause de la contention, l'AtomicInteger continuait activement de réessayer le CAS en boucle, jusqu'à y parvenir. A haut niveau de contention, cela pouvait tendre à être très lent.

Arrivent les LongAdder de Java 8. Cet ensemble de classes fournissent une manière pragmatique de lire et écrire des valeurs numériques de manière concurrente et à grande échelle. L'usage est super simple, instanciez juste un nouveau LongAdder et utilisez ses méthodes add() et intValue() pour incrémenter et lire le compteur.

La différence entre cette solution et les vieux Atomics, c'est qu'ici, quand un CAS échoue à cause de la contention, au lieu d'occuper le CPU, le Adder va stocker le delta dans une cellule interne allouée au thread courant. Il va par la suite ajouter cette valeur, ainsi que celle de toute autre cellule au résultat de intValue(). Cela réduit le besoin de revenir en arrière et faire un CAS ou bloquer d'autres threads.

Si vous vous posez la question de savoir quand vous devriez préférer les Adders concurrents aux Atomics pour gérer des compteurs, la réponse simple est - toujours.

3. Tri en Parallèle

Tout comme les Adders concurrents accélèrent les opérations de comptage, Java 8 délivre une manière concise d'accélérer le tri. La recette est assez simple.
Au lieu de :

Array.sort(myArray);

Vous pouvez désormais utiliser (notez le changement de classe utilisée) :

Arrays.parallelSort(myArray);

Cela va automatiquement séparer la collection cible en plusieurs fragments qui seront triés indépendamment sur un certain nombre de coeurs, puis regroupés. La seule limitation est que dans des environnements hautement multi-threadés, comme un conteneur web bien occupé, les bénéfices de cette approche vont commencer à diminuer (à plus de 90%), à cause du coût induit par l'augmentation des changements de contexte CPU.

4. Basculer sur la Nouvelle API Date

Java 8 introduit une API date et heure complètement nouvelle. Vous vous doutez bien qu'il était temps, quand la plupart des méthodes de l'API actuelle sont marquées comme dépréciées... La nouvelle API apporte au coeur de la librairie standard Java, une facilité d'utilisation et une précision qui ont longuement été fournies par l'API temporelle populaire Joda Time.

Tout comme toute nouvelle API, la bonne nouvelle c'est qu'elle est plus fonctionnelle et élégante. Malheureusement, il reste encore de vastes quantités de code dans la nature qui utilisent la vieille API, et ce n'est pas près de changer.

Pour aider à faire le pont entre les deux générations d'APIs, la vénérable classe Date a une nouvelle méthode appelée toInstant() qui convertit la Date dans la nouvelle représentation. Cela peut être spécialement efficace dans les cas où vous travaillez avec une API qui s'attend à la forme classique mais que vous voudriez apprécier tout ce que la nouvelle API a à offrir.

5. Contrôler les Processus de l'OS

Lancer un processus de l'OS depuis votre code est d'ores et déjà possible avec des appels JNI - c'est quelque chose que vous faites en sachant qu'il y a de fortes chances que vous obteniez des résultats inattendus et des exceptions vraiment mauvaises en chemin.

Malgré tout, c'est un mal nécessaire. Mais les processus ont une autre source de problèmes qui leur est associée - ils ont tendance à se suspendre. Le problème avec l'exécution de processus depuis du code Java, jusqu'à maintenant, c'est qu'il était difficile de contrôler un processus une fois qu'il était lancé.

Pour nous aider avec cela, Java 8 introduit trois nouvelles méthodes dans la classe Process :

  1. destroyForcibly tue un processus avec un degré de réussite beaucoup plus fort que précédemment.
  2. isAlive dit si un processus lancé par votre code est encore vivant.
  3. Une nouvelle surcharge pour waitFor() vous laisse spécifier la quantité de temps que vous voulez laisser au processus pour se terminer. Elle retourne si le processus s'est terminé avec succès ou s'il est tombé en time-out, auquel cas vous pourriez le tuer.

Deux bons cas d'utilisation pour ces nouvelles méthodes sont :

  • Si le process ne s'est pas terminé à temps, le terminer et continuer :
if (process.wait(MY_TIMEOUT, TimeUnit.MILLISECONDS)){
       //success! }
else {
    process.destroyForcibly();
}
  • S'assurer qu'avant que votre code ne se termine, vous ne laissez pas un processus derrière vous. Les processus suspendus peuvent lentement mais sûrement vider les ressources de votre OS :
for (Process p : processes) {
       if (p.isAlive()) {
             p.destroyForcibly();
       }
}

6. Opérations Numériques Exactes

Les dépassements numériques peuvent causer les bugs parmi les plus difficiles à cause de leur nature implicite. C'est spécialement vrai des systèmes où les valeurs entières int (comme les compteurs) grossissent au fil du temps. Dans ces cas là, ce qui marche bien en pré-production (et même pendant longtemps en production) peut subitement se mettre à dysfonctionner de manières bizarres, quand les opérations commencent à entrer en dépassement et à produire des valeurs complètement inattendues.

Pour aider, Java 8 a ajouté plusieurs nouvelles méthodes "exactes" à la classe Math, conçues pour protéger le code sensible de dépassements implicites, en lançant une exception non-vérifiée de type ArithmeticException quand la valeur d'une opération dépasse sa précision.

int safeC = Math.multiplyExact(bigA, bigB); // will throw ArithmeticException if result exceeds +-2^31

La seule limitation est que la tâche vous revient de trouver les endroits de votre code où de tels dépassements peuvent survenir. Pas du tout une solution auto magique, mais j'imagine que c'est mieux que rien.

7. Génération Aléatoire Sécurisée

Java s'est trouvé sous le feu des critiques pendant plusieurs années à cause de trous de sécurité. Qu'elles soient justifiées ou non, un gros travail a été fait pour fortifier la JVM et les frameworks contre de possibles attaques. La génération de nombres aléatoires avec un faible niveau d'entropie, rend les systèmes utilisant ces générateurs aléatoires pour la création de clés de chiffrement ou de données sensibles au hash, plus ouverts au hacking.

Jusqu'à présent, la sélection de l'algorithme de Génération de Nombres Aléatoires (Random Number Generation) était laissé au développeur. Le problème est que lorsque l'implémentation dépend d'un matériel / OS / JVM spécifique, l'algorithme désiré peut ne pas être disponible. Dans ce cas, les applications ont une tendance à se retourner vers des générateurs plus faibles, ce qui peut les exposer à un plus grand risque d'attaque.

Java 8 a ajouté une nouvelle méthode appelée SecureRandom.getInstanceStrong() qui vise à forcer la JVM à choisir un fournisseur sécurisé pour vous. Si vous écrivez du code sans avoir le contrôle complet de l'OS / matériel / JVM sur lequel il s'exécutera (ce qui est très courant lorsqu'on déploie dans le Cloud ou sur un PaaS), ma suggestion est de donner à cette approche toute la considération qu'elle mérite.

8. Références Optionnelles (Optional)

Les NullPointers, c'est comme se cogner les doigts de pieds - vous le faites depuis que vous tenez debout, et aussi intelligent que vous soyez aujourd'hui, il y a de fortes chances que ça vous arrive toujours. Pour aider sur ce vieux problème, Java 8 introduit un nouveau modèle appelé Optional<T>.

Emprunté à Scala et Haskell, ce modèle est destiné à explicitement déclarer quand une référence passée à ou retournée par une fonction, peut être nulle. Il s'agit de réduire le jeu des devinettes sur les références potentiellement nulles, quand on s'appuie trop sur une documentation qui peut ne plus être à jour, ou la lecture du code qui peut changer au cours du temps.

Optional<User> tryFindUser(int userID) {

ou :

void processUser(User user, Optional<Cart> shoppingCart) {

Le modèle Optional a un ensemble de fonctions qui rendent son évaluation plus pratique, comme par exemple isPresent() pour vérifier si une valeur non-null est disponible, ou ifPresent() auquel on passe une fonction Lambda qui sera exécutée si isPresent est vrai. La limitation, c'est que tout comme les nouvelles APIs de date et heure de Java 8, il faudra du temps et du travail pour que ce modèle prenne racine et soit absorbé dans les bibliothèques que nous utilisons et concevons au quotidien.

Nouvelle syntaxe Lambda pour imprimer une valeur optionnelle :

value.ifPresent(System.out::print);

 

A Propos de l'Auteur

Tal Weiss est CEO de Takipi. Tal a conçu des applications Java et C++ scalables, temps-réel, pendant les 15 dernières années. Il continue cependant d'apprécier l'analyse d'un bon bug, ainsi que l'instrumentation de code Java. Pendant son temps libre, Tal joue de la batterie Jazz.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT