|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
Questo capitolo contiene le sezioni di approfondimento di alcuni concetti che sono stati solo accennati nel corso di questo tutorial. La lettura di queste sezioni è opzionale nel senso che i concetti esposti non si applicano al linguaggio Java in modo specifico.
Tradotto da Introducing JSON:
Le caratteristiche principali di JSON sono le seguenti;
Prendiamo come esempio il seguente messaggio di presentazione di MasterMind:
type: 103 id: 10 field[0]: Lukas (il nome del giocatore) field[1]: 66051 (la versione della applicazione)Nel formato descritto in Il formato del messaggio appare come segue:
MM0;103;10;Lukas;66051;null;null;FILLER
Di seguito, il confronto tra come apparirebbe in XML ed in JSON:
XML JSON
--------------------------- --------------------
<?xml version="1.0" |{
encoding="UTF-8"?> | "type" : 103,
<messaggio> | "id" : 10,
<type>103</type | "fields" :
<id>10</id> | [
<fields> | "Lukas",
<field>Lukas</field>| 66051,
<field>66051</field>| null,
<field>null</field> | null
<field>null</field> | ]
</fields> |}
</messaggio> |
Come potete osservare da soli, non c'è paragone di leggibilità tra i due formati: JSON si legge al volo mentre XML non è così intuitivo. Per approfondire la sintassi di JSON ed imparare ad usarlo leggete il link di cui sopra o cercate nel web: ci sono migliaia di articoli interessanti.
Nei sistemi informatici ci sono diversi orologi ognuno con la propria specifica funzione:
Il system clock (=orologio di sistema) è la frequenza di funzionamento del microprocessore e si misura in Herz: questo valore non ha nulla a che fare con il elapsed time (=tempo trascorso) in quanto dipende dal microprocessore
Per misurare il tempo trascorso i sistemi possiedono un system timer (un timer di sistema) il cui "tichettio" aggiorna un contatore seriale; questo contatore misura i secondi trascorsi da una data che, per convenzione, viene posta al 1 gennaio 1970. Questa data è detta epoch (=epoca).
In Java, il timer di sistema è rappresentato dalla classe java.time.Instant ma è possibile ottenere il valore del timer in millisecondi con il metodo statico System.currentTimeMillis.
Vi è da osservare che il timer di sistema garantisce una precisione al millisecondo ma non la sua granularità: può essere che il "tichettio" del timer non sia al millisecondo, ma anche più fine (decimi di millisecondo) o più grossa, dipende dal sistema.
La libreria Java definisce il metodo statico System.nanoTime che restituisce il tempo trascorso, in nanosecondi (miliardesimi di secondo) dalla esecuzione della Java Virtual Machine corrente.
Questo valore si affida ad un orologio ad alta risoluzione, se esso esiste nel sistema, altrimenti la sua granularità è la stessa del timer di sistema.
Il timer ad alta risoluzione viene usato per misurare il tempo di esecuzione delle applicazioni o, meglio, di alcuni dei suoi metodi che dipendono fortemente dai tempi.
Il timer ad alta risoluzione non è adatto a misurare il tempo inteso come data e ora corrente: ci sono classi dedicate a questo.
Per misurare il trascorrere del tempo inteso come data e ora, Java mette a disposizione la classe java.time.LocalDateTime che ha un range di utilizzo di un paio di miliardi di anni e usa il formato ISO-8601, senza fuso orario, per la rappresentazione. Per esempio la stringa 2024-12-03T10:15:30 indica le ore 10:15:30 UTC del 3 dicembre 2024.
La classe LocalDateTime usa, per gli anni bisestili, le regole del calendario gregoriano, quello introdotto da Papa Gregorio XIII nel 1582. Queste regole vengono applicate a tutte le date sia nel futuro che nel passato e questo può andare bene per la maggior parte delle applicazioni moderne ma daranno risultati imprecisi per le date antecedenti la riforma gregoriana dal momento che, fino al 4 ottobre 1582 era in vigore il calendario giuliano, introdotto da Caio Giulio Cesare nel 46 a.C, che aveva regole diverse per gli anni bisestili.
Nella sezione La sincronizzazione abbiamo imparato che alcune operazioni sono critiche e che devono essere portate a termine senza interruzioni da parte di threads separati altrimenti si corre il rischio di incappare nel problema della inconsistenza della memoria. Come abbiamo imparato nella sezione a riferimento, i metodi che eseguono queste operazioni critiche devono essere sincronizzati: l'uno non deve mai interrompere l'altro.
La strategia adottata per sincronizzare i metodi è quella dei semafori. Lo stesso linguaggio Java mette a disposizione del programmatore una classe specializzata a questo scopo: java.util.concurrent.Semaphore. Il semaforo funziona più o meno così:
Il metodo aspetta che il semaforo diventi VERDE e, qundo accade, lo imposta a ROSSO in modo che qualunque altro thread che usa un metodo sincronizzato resti bloccato. Alla fine delle operazioni il semaforo viene nuovamente impostato a VERDE. Il codice suesposto, benchè apparentemente perfetto, NON FUNZIONERA' MAI. Il problema sta nel fatto che due threads distinti potrebbero eseguire il ciclo while nello stesso identico istante e, trovando il semaforo VERDE, lo impostano a ROSSO procedendo entrambi nelle operazioni. Certo che le probabilità di un simile evento sono scarse, tendenzialmente prossime allo ZERO ma, come diceva un grande saggio: "qualsiasi evento, per quanto improbabile,
dato un numero sufficiente di occasionhi, si verificherà".
Pertanto, una applicazione che si basa su un semaforo implementato con il codice suespoto potrebbe funzionare per mesi, anni o anche decenni ma, prima o poi, andrà in crash.
Il problema è che le due operazioni di verifica ed impostazione del semaforo devono essere eseguite in modalità atomica, cioè senza alcuna interruzione potenziale; trattandosi però di due istruzioni separate, non potranno mai essere eseguite in modalità atomica. Nessun linguaggio di alto livello è in grado di eseguire le due operazioni in modalità atomica; ma il codice macchina nativo può farlo.
Tutti i microprocessori possiedono nel loro set di istruzioni la XCHG che è il codice assembly per la istruzione eXCHanGe (=scambia) il cui compito è quello di scambiare il contenuto di due operandi: di solito, uno degli operandi è un registro del processore e l'altro è una cella di memoria cioè una variabile che rappresenta il semaforo. Scritto in pseudo-codice potrebbe apparire così:
Poichè il microprocessore garantisce che ogni singola istruzione in codice macchina viene eseguita in modalità atomica (quindi se una istruzione viene iniziata il processore garantisce che sarà completata senza interruzioni), il semaforo può essere implementato in questo modo:
Dopo lo scambio avremo due possibili situazioni: