PDA

View Full Version : [Java] Gestione Threads


CertainDeath
12-10-2009, 15:48
Salve ragazzi, devo realizzare un programma in Java che mediante la gestione dei threads incrementa e decrementa una variabile.
Mi son costruito 4 classi.. sono alle prime armi e nonostante il problema sia semplice per me non è immediatissimo.

Il main:

public class main {
public static void main(String[] args) {
Thread thread = new Thread();
Incrementa incrementa = new Incrementa(thread);
Decrementa decrementa = new Decrementa(thread);
Thread aggiungo = new Thread(incrementa);
Thread sottraggo = new Thread(decrementa);
aggiungo.start();
sottraggo.start();

try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
}

}


public class Thread {
int numeri = 0;

public void plus() {
numeri++;
}

public void minus() {
numeri--;
}
}



public class Incrementa implements Runnable {
Thread th;

public Incrementa(Thread th) {
this.th = th;
}

public void run() {

th.plus();
}

}



public class Decrementa implements Runnable {
Thread th;

public Decrementa(Thread th) {
this.th = th;
}

public void run() {

th.minus();
}

}


Ringrazio tutti coloro i quali aiuteranno un nabbo come me :muro:

PGI-Bis
12-10-2009, 16:43
Cambia il nome della tua classe Thread in Contatore.

Aggiungi il modificatore volatile al campo numeri di Contatore:

private volatile int numeri = 0;

Dopodichè il programma aumenta e diminuisce di uno il valore di contatore e termina.

public class Main {

public static void main(String[] args) {
Contatore contatore = new Contatore();
Incrementa incrementa = new Incrementa(contatore);
Decrementa decrementa = new Decrementa(contatore);
Thread aggiungo = new Thread(incrementa);
Thread sottraggo = new Thread(decrementa);
aggiungo.start();
sottraggo.start();
try {
Thread.sleep(1000);
} catch(InterruptedException ex) {
ex.printStackTrace();
return;
}
System.out.println("Contatore vale: " + contatore.numeri);
}
}

class Contatore {
volatile int numeri = 0;

public void plus() {
numeri++;
}

public void minus() {
numeri--;
}
}

class Incrementa implements Runnable {

Contatore th;

Incrementa(Contatore th) {
this.th = th;
}

public void run() {
th.plus();
}
}

class Decrementa implements Runnable {

Contatore th;

Decrementa(Contatore th) {
this.th = th;
}

public void run() {
th.minus();
}
}

CertainDeath
12-10-2009, 18:57
Cosa sta a significare "Volatile" non l'ho mai sentita come istruzione.. Grazie della precisa risposta :D

La cosa strana che rimane come valore sempre 0.. perchè? non incrementa la variabile?

PGI-Bis
12-10-2009, 21:50
volatile serve per obbligare i thread a leggere il valore di un campo dalla memoria condivisa e per obbligare gli stessi thread ad aggiornare quella memoria quando cambiano il valore di quel campo.

Senza volatile, e non usando altri meccanismi di sincronizzazione, quando un thread "legge" un campo è libero di non tener conto del valore che quel campo ha e, quando "scrive su" un campo, è libero di non aggiornare la regione di memoria condivisa corrispondente a quel campo.

PGI-Bis
12-10-2009, 21:54
La cosa strana che rimane come valore sempre 0.. perchè? non incrementa la variabile?

Il valore iniziale del campo è zero. Il Thread incrementa aumenta quel valore di uno, il Thread decrementa lo riduce di uno, il valore che risulta non può che essere zero.

Nota che i due Thread non eseguono dei cicli di incremento o decremento: uno fa un +1 e schiatta subito dopo, l'altro fa un -1 e si defila altrettando rapidamente. E' come se ci fosse scritto:

int valore = 0;
valore++
valore--;
System.out.println(valore);

CertainDeath
12-10-2009, 22:10
Per fare dei cicli di incremento e decremento posso usare Synchronized? oppure devo usare i cicli if.. ecc?

PGI-Bis
12-10-2009, 22:40
I cicli li fai coi cicli :D.

synchronized, volatile, pipicchio e pipacchio ti interessano solo nel momento in cui riscontri che il tuo Thread sta cercando di accedere ad un valore che non è stato lui a creare.

Tutto il resto è immutato, i for sono for, gli if sono if, gli interi sono interi eccetera.

nuovoUtente86
13-10-2009, 00:19
Secondo le specifiche però, in questo caso, volatile non dovrebbe garantire l' atomicità in quanto il valore della variabile dipende dal precedente (trattandosi di un contatore).

PGI-Bis
13-10-2009, 01:05
L'unica atomicità garantita da volatile è sulle letture o scritture dei valori a 64bit. Per il resto tutte le letture e scritture sono atomiche di loro.

Che quello sia un contatore è tutto da verificare: ho messo il nome che m'è venuto ma in origine era un Thread.

nuovoUtente86
13-10-2009, 01:40
Ho ripreso il termine contatore giusto perchè lo avevi già utilizzato: potrebbe rappresentare un' istantanea degli utenti su una coda o qualsiasi altra cosa ma non è questa la cosa importante.
Sicuramente read e write (tranne che per long e double)sono garantiti dalla jvm, ma numeri++(numeri=numeri+1) e numeri-- non sono eseguite in maniera indivisibile (come invece garantisce qualche compilatore c++).
In questo caso il risultato risulta corretto forse più per la sincronizzazione sull' ultima istruzione del thread che lancia (ma non ne sono affatto sicuro) che per l' utilizzo di volatile. Se mettissimo i cicli e lanciassimo più di 2 thread il risultato non credo sarebbe coerente.

PGI-Bis
13-10-2009, 12:59
Come dicono i francesi, calma e gesso :D.

A me volatile interessa perchè senza quel "System.out.println(contatore.numeri)" può non riflettere le mutazioni che i due altri thread producono su "numeri".

Dopodichè, assumendo che i due thread aggiungo e sottraggo terminino la loro esecuzione in quel secondo di attesa del main, stamperà o zero o uno o meno uno.

nuovoUtente86
13-10-2009, 13:53
Su questo non ci piove, ma presumo che intrinsecamento lo scopo dell' esercizio fosse anche attuare una politica di sincronizzazione, che molto grezzamente (forzando alla fine la sequenzialità) poteva essere garantita imponendo una join(), ma meglio con l' utilizzo di AtomicInteger.
A proposito del package Atomic volevo chiederti se lo stesso garantisce l' atomicità anche su architetture multicore ovvero se un eventuale thread che esegue su un altro core venga sospeso se tenta l' accesso ad una variabile impegnata in una operazione atomica non ancora conclusa?

PGI-Bis
13-10-2009, 14:16
Sì, gli AtomicXYZ definiscono anche operazioni atomiche nel senso che i vari getAndSet, setAndGet, addAndSet eccetera sono unitarie.

A meno di bug - e nella bugparade qualcuno ce n'era - l'architettura è sempre irrilevante. Se noti tutto ciò che si trova nel package concurrent spiega i propri effetti in relazione alle norme del java memory model e il JMM dice che se una cosa si chiama "Java" allora deve fare una certa cosa, a prescindere dal sistema ospite.

Come si faccia concretamente è esemplificato in

http://gee.cs.oswego.edu/dl/jmm/cookbook.html

nuovoUtente86
13-10-2009, 14:45
si effettivamente far dipendere il comportamento dalla piattaforma avrebbe negato l' essenza stessa di JAva.
Ragionando un po sui thread, mi è sorto un dubbio ovvero se nella work area privata di ogni thread, in caso di oggetti che ne puntano altri, questi vengano mantenuti i riferimenti (quindi mantenuti in shared) oppure si clonano effettivamente nella zona privata tutti gli oggetti presenti sulla catena di reference

PGI-Bis
13-10-2009, 15:06
La metafora della copia vale solo per il valore delle variabili condivise che, per un reference, sarebbe il valore del puntatore. Eventuali riferimenti indiretti collegati a quel puntatore sono a loro volta soggetti alle norme del modello di memoria. E' il tipico caso della variabile locale. Se ho un thread che dichiara e inizializza localmente un punto:

new Thread() {
public void run() {
Point p = new Point(10, 20);
altroThread.valuta(p);
}
}.start();

Il modello di memoria dice che essendo "p" locale il secondo thread che riceve p attraverso valuta non vedrà mai p valere null ma sempre p che vale un certo punto.

Il problema è che se p è locale NON sono locali i campi x e y accessibili tramite p e dunque il loro valore sarà o non sarà 10 e 20 a seconda che per x e y si possano dire rispettate le norme sulla sincronizzazione delle letture con le scritture - nel caso specifico di Point non lo sono e l'unica garanzia che si ha è che p sia "properly constructed", cioè x e y varranno certamente almeno zero.

E' lì che entra in gioco la famosa relazione "happens-before" che va sempre valutata sia nel rapporto intre-thread che nel rapporto intra-thread. Ad esempio se in un Thread A avessimo:

Point p = new Point();
p.y = 300;
synchronized(p) {
p.x = 10;
}
threadB.valuta(p);

...ThreadB.valuta
synchronized(p) {
int x = p.x;
int y = p.y
}

ThreadB vedrebbe sia x valere 10 sia y valere 300. Vede x = 10 perchè l'entrata di ThreadB nel blocco sincronizzato su p obbliga la lettura dell'ultimo valore scritto in un blocco sincronizzato su p. Vede y = 300 perchè nel Thread A tra y = 300 e x = 10 c'è una relazione happens-before e questa relazione è sempre transitiva.

nuovoUtente86
13-10-2009, 19:11
Quindi se abbiamo un array di int su cui operano diversi thread, la "copia" locale ad ogni thread riguarda il reference all' oggetto piuttosto che il vettore stesso.
L' esempio non è casuale, perchè ricordo una vecchia discussione, su un altro forum, in cui si parlava proprio di assegnamenti del tipo
v[posizione]=10;
non scaricati sulla memoria dell' applicazione, eppure mantenendo copia solo del reference non dovrebbe sussistere il problema.

PGI-Bis
13-10-2009, 19:28
E' tutto locale, nel senso che se ho un riferimento ad un array

x

senza sincronismi ogni thread ha il suo riferimento x. Quando il thread agisce sul componente [0] agisce su una copia locale del componente di un array locale.

Se garantiamo la condivisione del riferimento all'array x allora ogni volta che il thread va a leggere x legge l'ultimo valore che questo x ha (cioè se qualcuno redirige il riferimento verso un altro array allora il thread agirà su quell'array). Questo però non si estende ai componenti di quell'array:

x[1] sarà un valore locale che il thread non è tenuto ad inizializzare con lo stesso valore che ha l'array a cui x punta.

Esempio.

final int[] array = new int[20];

final ci dice che chiunque legga array lo vedrà come array di 20 componenti ognuno dei quali vale zero.

1) thread 1: array[0] = 20;
2) thread 2: System.out.println(array[0]);

thread 2 può stampare zero perchè quando thread 1 scrive sul componente 0 di array non è tenuto a far sì che tale scrittura sia visibile ai thread che leggerano quel valore.

E questo nonostante sia thread 1 che thread 2 vedano "array" puntare alla stessa regione di memoria.

PGI-Bis
13-10-2009, 19:39
Qui c'è una serie molto carina di Sun sulla programmazione parallela

http://channelsun.sun.com/video/an+introduction+to+parallel+programming+/25947206001

la segnalo perchè c'è un punto in cui si "vede" questa faccenda della località dei riferimenti detenuti dai thread nelle architetture multicore dove lungo la strada che dal programma nella memoria principale arriva al singolo core a un certo punto la memoria cessa di essere condivisa e "ognuno fa da sè".

nuovoUtente86
13-10-2009, 20:21
E se usassimo un' ArrayList di Integer in luogo dell' array avremmo lo stesso comportamento?

PGI-Bis
13-10-2009, 21:03
Sì, vale in generale per ogni cosa che si trovi nella memoria condivisa.

nuovoUtente86
13-10-2009, 21:12
Ora la situazione si sta schiarendo ma resta ancora qualche dubbio e riprendo il tuo esempio di prima:
se abbiamo
int[] array = new int[20];

la variabile array conterrà l' indirizzo, sull' heap del programma, in cui il vettore è ospitato.
I thread che opera sull' array avranno una variabile di tipo reference array_privato che se ho ben capito(ed è proprio questo il dubbio), punterà (almeno inizialmente) proprio all' indirizzo sull' heap globale. Però a questo punto non capisco dove vengono fatte le scritture del tipo array[0]=1; che hanno scope locale ai thread.

PGI-Bis
13-10-2009, 22:08
Su variabili locali al thread, di tipo uguale a quello del componente dell'array, il cui valore iniziale è il valore predefinito dei campi non inizializzati.

Occhio che senza alcun intervento esterno quando l'array deve operare su "array" non è tenuto ad andare a vedere cosa c'è nella regione di memoria a cui punta la variabile array - cioè il valore della variabile.

E' solo obbligato ad operare su una variabile del tipo di array. Cioè:

int[] array = new int[20];

...
Thread 1: array[0] = 20; //può benissimo rilasciare una NullPointerException

Mentre:

final int[] array = new int[20];
...
Thread 1: array[0] = 20; //non rilascerà mai una NullPointerException

Il final obbliga Thread 1: ad andare a guardare dove punta array, e non punta a null. Vedrà che ha dei campi. Non è obbligato ad andare a vedere il valore di quei campi. Quindi se io dicessi:

final int[] array = new int[20];
array[0] = 300;
...
Thread 1: print(array[0]); //Mai NullPointer ma può stampare zero anzichè 300

Vale anche per volatile, sebbene questo sia più "forte" di final:

volatile int[] array = new int[20]
array[0] = 300;

Le specifiche dicono che la scrittura di un valore volatile happens-before la lettura di quel valore.

"A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread"

Qui "v" è "array".

Per la transitività, ciò che in uno stesso thread capita prima della scrittura di un valore volatile capita anche prima della lettura di quel valore. Ma è sempre lettura o scrittura del valore della variabile volatile. E se andiamo a guardare l'operazione di assegnamento ad un componente di un array vediamo che:

1. legge il valore "array"
2. non scrive sul valore "array" ma attraverso il valore array sul campo [0]

C'è una lettura di "array" (il valore volatile) che precede una scrittura sul campo [0]. Quando, più tardi, accade:

Thread 1: int y = array[0];

Essendo array volatile Thread 1 è obbligato a vedere il valore che array ha in forza dell'ultima scrittura. E l'ultima scrittura sulla variabile "array" è "new int[20]": la scrittura su array - e non tramite array - è il punto che, per una variabile volatile - definisce l'ordine di sincronizzazione.

Paradossalmente, per via della semantica di volatile e della transitività, obbligheremmo Thread 1 a vedere 300 se dicessimo:

volatile int[] array = new int[20];
array[0] = 300;
array = array;//scrittura su array
...
Thread 1: println(array[0]); //deve stampare 300

Deve stampare 300 perchè:

1. le azioni intra-thread capitano nell'ordine in cui appaiono nel codice.

Quindi array[0] = 300; happens-before array = array.

2. la scrittura di un valore volatile da parte di un thread A happens-before la lettura su quel valore da parte di un thread B:

quindi in Thread 1 "array" (la lettura) capita dopo array = array che capita dopo array[0].

in Thread 1, array[0] capita dopo la lettura di array. Assumendo che Thread 1 esegua le sue istruzioni dopo il thread che dichiara e e cambia il valore di array, il risultato che vedremo dovrà essere quello che avremmo se eseguissimo questo unico flusso:

volatile int[] array = new int[20];
array[0] = 300;
array = array;//scrittura su array
println(array[0]);


Effetto "accademico" di volatile perchè, come per l'incremento ++ o il decremento --, in un contesto più realistico non è possibile assumere che un Thread interverrà su array solo dopo "array = array" ma si introdurranno degli altri meccanismi di sincronizzazione che, in genere, garantiscono la visibilità e qualcos'altro.

nuovoUtente86
13-10-2009, 22:46
la variabile array conterrà l' indirizzo, sull' heap del programma, in cui il vettore è ospitato.
I thread che opera sull' array avranno una variabile di tipo reference array_privato che se ho ben capito(ed è proprio questo il dubbio), punterà (almeno inizialmente) proprio all' indirizzo sull' heap globale.

Quindi questo ragionamento non è del tutto corretto? O meglio lo è ma come dicevi, se ho ben capito, i thread non hanno l' obbligo di sincronizzarsi sul valore globale ma sono tenuti solo al corretto utilizzo dei tipi.
Quindi se io avessi
ArrayList ar =new ArrayList(10);
for(......)
ar.add(...);

e un thread deve operare un' aggiunta
ar.add(new Integer(10));
potrebbe benissimo farla su una ArrayList vuota.

PGI-Bis
13-10-2009, 22:57
Non è corretto. La regola è che un qualsiasi Thread è tenuto a vedere un qualsiasi campo inizializzato al suo valore di default (0, null, false).

Poi si va a vedere se sussistono delle condizioni particolari che obbligano il Thread a leggere il valore effettivo di quel campo.

Vale per i campi singolarmente considerati. Se A è un campo attraverso cui è possibile accedere ad altri campi, un eventuale obbligo per un thread di vedere il valore attuale di A non comporta necessariamente l'obbligo di vedere i valori attuali dei campi accessibili tramite A.

PGI-Bis
13-10-2009, 22:59
ArrayList ar =new ArrayList(10);
for(......)
ar.add(...);

e un thread deve operare un' aggiunta
ar.add(new Integer(10));
potrebbe benissimo farla su una ArrayList vuota.

Può cercare di farlo su un "ar" che vale null.

Se per una qualche ragione (final, volatile o altro) è tenuto a vedere il valore di "ar" ciò non significa che sia tenuto a rispettare il valore dei campi accessibili tramite "ar".

nuovoUtente86
14-10-2009, 00:27
Quindi in realtà dichiarare una variabile reference volatile o sincronizzarla con qualche altro strumento del linguaggio garantisce che, un eventuale riassegnamento (ar=new ArrayList), sia visto da tutti i thread concorrenti ma non assicura nulla circa operazioni come add e remove ad esempio.
Mi viene ora in mente che le comuni implementazioni del produttore/consumatore(classi esempio di multithreading) potrebbero (in realtà quasi tutte le implementazioni poi funzionano) non funzionare per via dell' utilizzo del buffer private.
Ma a questo punto come garantire che operazioni come array[0]=1; o arrayl.add(qualcheOggetto); abbiano effettivamente visibilità sull' heap globale?

PGI-Bis
14-10-2009, 01:10
E' corretto dire che volatile non garantisce in assoluto la visibilità delle mutazioni che coinvolgono riferimenti indirettamente accessibili attraverso la variabile volatile.

Tuttavia rendere quelle mutazioni visibili è piuttosto facile.

Considera ad esempio synchronized. Il JMM dice che l'uscita da un blocco sincronizzato su un monitor M happens-before l'entrata in un blocco sincronizzato sullo stesso monitor M - da parte di un altro Thread.

Supponiamo che due Thread condividano lo stesso monitor M, sia esso un riferimento ad hoc

final Object M = new Object(); //sempre final

o un Lock:

final Lock M = new ReentrantLock();

o lo stesso reference che un Thread vuole passare ad un altro, a patto che ne sia un riferimento locale. Per via della transitività della relazione HB questo:

final Object M = new Object();

1.) Thread 1: Point p = new Point(10, 20);
2.) Thread 1: ArrayList<Point> list = new ArrayList<Point>();
3.) Thread 1: list.add(p);
4.) Thread 1: synchronized(M) {}

seguito da:

5.) Thread 2: synchronized(M) {}
6.) Thread 2: Point x = list.get(0);
7.) Thread 2: System.out.println("(" + p.x +"," + p.y + ")");

stampa sempre

(10, 20)

Proprio per via della transitivià. L'entrata di Thread 2 nel blocco sincronizzato è successiva, quanto alla visibilità delle letture e delle scritture, all'uscita da quel blocco da parte di Thread 1. Tutte le letture e scritture che Thread 1 esegue prima di uscire dal blocco sincronizzato avvengono in uno stesso thread quindi accadono nell'ordine in cui si vedono nel codice. Tutte le scritture e letture che Thread 2 esegue dopo l'entrata nel blocco sincronizzato sono parimenti operazioni eseguite in un solo thread quindi capitano nell'ordine del codice.

E' la transitività della relazione happens-before a rendere le cose facili.

Il problema è che la sincronizzazione richiede operazioni molto costose lato CPU - si parla delle più onerose in assoluto.

Non è sempre necessaria. Ad esempio sappiamo dal JMM che l'azione che avvia un thread happens-before la prima azione (cioè la prima lettura o scrittura) eseguita dal nuovo Thread.

Ciò significa che un caso come questo:

List<Point> list = new ArrayList<Point>(); //ipotetico campo
...crea un marea di punti e infilali in list

new Thread() {
public void run() {
for(Point p : list) {
fai qualcosa con p
}
}
}.start();

Non richiede alcun synchronized, final o volatile per funzionare, perchè "start" capita prima della prima istruzione nel nuovo Thread e tutte le operazioni sulla lista capitano prima di quello start - perchè sono azioni intra-thread.

E ci sono poi le utilità del package collection. Una delle garanzie di ConcurrentLinkedQueue o di CopyOnWriteArrayList eccetera è che i "put" e gli "add" capitano prima dei "get". Quindi data un'ipotetico campo-coda:

final ConcurrentLinkedQueue<Point> coda = new ConcurrentLinkedQueue<Point>();

Thread 1: Point p = new Point();
Thread 1: p.x = una tragedia di conti che coinvolge milioni di altri campi
Thread 1: coda.offer(p);

e letto in seguito da un altro

Thread 2: Point p = coda.take();

è "correttamente sincronizzato", nel senso che i valori x e y, indirettamente accessibili tramite p, saranno letti per il valore che effettivamente hanno nell'heap.

perchè il campo coda deve essere final? Perchè ConcurrentLinkedQueue dice che "take" capita dopo "offer" ma prima di "take" Thread 2 compie un'azione, la lettura del campo "coda", e questa non è successiva al take.

A conti fatti bisogna semplicemente prestare attenzione a qualche piccolo dettaglio ma il grosso della creazione di programmi multithread Java correttamente sincronizzati è relativamente gestibile.

E non c'è il problema di andare a guardare come funzioni questo o quel compilatore, questa o quella architettura.

nuovoUtente86
14-10-2009, 01:41
Proprio per via della transitivià. L'entrata di Thread 2 nel blocco sincronizzato è successiva, quanto alla visibilità delle letture e delle scritture, all'uscita da quel blocco da parte di Thread 1. Tutte le letture e scritture che Thread 1 esegue prima di uscire dal blocco sincronizzato avvengono in uno stesso thread quindi accadono nell'ordine in cui si vedono nel codice. Tutte le scritture e letture che Thread 2 esegue dopo l'entrata nel blocco sincronizzato sono parimenti operazioni eseguite in un solo thread quindi capitano nell'ordine del codice.

Parto dalle premessa che abbiamo fatto qualche post fa ovvero che un thread garantisce solo di accedere alle "copie" private inizializzate ai valori di defautl. Quindi presumo o meglio deduco dal tuo ultimo post che siano gli strumenti di sincronizzazione a garantire che il thread2 stampi le coordinate del punto (contenute sul segmento globale)e non faccia una system.out su un punto che punta a NULL.

final ConcurrentLinkedQueue<Point> coda = new ConcurrentLinkedQueue<Point>();

Thread 1: Point p = new Point();
Thread 1: p.x = una tragedia di conti che coinvolge milioni di altri campi
Thread 1: coda.offer(p);

e letto in seguito da un altro

Thread 2: Point p = coda.take();

è "correttamente sincronizzato", nel senso che i valori x e y, indirettamente accessibili tramite p, saranno letti per il valore che effettivamente hanno nell'heap.
anche in questo caso credo sia il locking interno a forzare il flushing dei dati sull' heap?!
Perchè l' happens before ci assicura l' ordine delle azione, ma non credo che indichi nulla circa la località o globalità dei dati.

PGI-Bis
14-10-2009, 13:18
Mhh... non ho capito la domanda :fagiano:

Dal punto di vista del linguaggio la ragione per cui quel codice, nel presupposto che Thread 2 esegua quelle istruzioni dopo Thread 1, è correttamente sincronizzato, deriva dal fatto che sussitono le relazioni happens-before citate.

Spetta poi alla macchina virtuale fare in modo che quelle regole del linguaggio di programmazione Java siano "fisicamente" rispettate.

La località è metaforica. Una JVM che semplicemente dica ai Thread di andare sempre e comunque ad aggiornare la memoria condivisa rispetta il modello di memoria. E non ci sarebbe alcuna località. Ma anche una JVM che solo sull'offer e sul take emettesse un'istruzione (mfence o mf o mb o msync) rispetterebbe l'happens-before mentre le istruzioni precedenti e successive sarebbero libere di agire sui valori eventualmente contenuti nella cache del processore.

Non so però se questo fosse il dubbio.

nuovoUtente86
14-10-2009, 15:27
Non richiede alcun synchronized, final o volatile per funzionare, perchè "start" capita prima della prima istruzione nel nuovo Thread e tutte le operazioni sulla lista capitano prima di quello start - perchè sono azioni intra-thread.

provo a dare un' interpretazione grossolana: all' atto dello start il thread si sincronizza(anche sui valori referenziati indirettamente) con la situazione heap creata dalle operazioni del thread che lo lancia che più formalmente dovrebbe essere questa parte di specifica

An action that starts a thread synchronizes-with the first action in the thread it starts.
The write of the default value (zero, false or null) to each variable synchronizes-with the first action in every thread. Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.

Che poi la seconda parte dovrebbe essere quella relativa ai valori di default già anticipata qualche post fa.
spero di non aver detto schiocchezze.

è "correttamente sincronizzato", nel senso che i valori x e y, indirettamente accessibili tramite p, saranno letti per il valore che effettivamente hanno nell'heap.

perchè il campo coda deve essere final? Perchè ConcurrentLinkedQueue dice che "take" capita dopo "offer" ma prima di "take" Thread 2 compie un'azione, la lettura del campo "coda", e questa non è successiva al take.
ho compreso il discorso sull' happens before, ma il dubbio è perchè l' istruzione coda.offer(p), che deve avvenire prima di take() (e fin qui ci siamo), scarica i valori correnti sull' heap e non su una sua variabile privata, come invece abbiamo assunto per il v[1]=10; sul vettore dei precedenti post.
Quello che mi viene da pensare è che le regole di happens-before costituiscono punto di sincronizzazione.

PGI-Bis
14-10-2009, 15:48
Il perchè è, banalmente, perchè lo dice la javadoc, cioè fa parte del contratto delle collezioni concurrent. Poi se andiamo a guardare il codice, ad esempio di CopyOnWriteArrayList, vediamo che per certe operazioni usa un Lock, il cui contratto dichiara un'equivalenza semantica con synchronized(X), per altre si avvale di condizioni happens-before stabilite nel JMM che non richiedono l'uso di modificatori o primitive di sincronizzazione (un po' come thread.start() è sincronizzato con la prima lettura o scrittura eseguita dal thread senza il bisogno di ulteriori aggiunte).

Lo stesso fa coda.offer. Codice alla mano, usa AtomicReferenceUpdater che è una "capsula" in grado di garantire la visibilità inter thread delle mutazioni occorse ai campi di un dato riferimento.

Ma sono tutti dettagli di realizzazione, quello che interessa a noi "utenti", del linguaggio o delle sue librerie, è che talune operazioni sono, in senso lato, "thread-safe".

Quello che mi viene da pensare è che le regole di happens-before costituiscono punto di sincronizzazione.

E' il contrario ma siamo lì. il JMM parte dicendo che si sono alcune operazioni definite come actions - principalmente letture e scritture.

Di queste talune sono intra-thread, talaltre inter-thread.

A certe condizioni (lo start, il final, il volatile, il synchronized, lo yield, il wait-notify eccetera) le azioni inter-thread sono da considerarsi sincronizzate.

Da ultimo, se un'azione A è sincronizzata con un'azione B allora l'implementazione deve fare in modo che l'esecuzione del programma manifesti l'accadimento di A prima di B.

Come è irrilevante: se un thead dice che x vale uno e un thread dice che x vale x + 1 e dal codice sorgente risulta che queste due espressioni accadono una prima dell'altra - perchè comportano azioni, quelle azioni sono sincronizzate, e quindi A happens-before B - allora in un qualsiasi modo alla fine x dovrà valere 2.

nuovoUtente86
14-10-2009, 16:25
public class Test{
public static void main(String[]args){
int[] array={10,10,10,10,10};
new ProvaThread(array,1).start();
new ProvaThread(array,2).start();
}}

in questo caso dovrei aver la garanzia che sia thread1 che che thread2 si soncronizzino con il main e quindi vedano l' array inizializzato correttamente e se nel run di ognuno facessi una stampa di uno degli elementi otterrei 10. E' corretto?
Se però invece di una stampa facessi un' assegnamento array[1]=7; che comportamento si avrebbe?

e ancora se modificassi il codice con qualcosa del genere

public class Test{
public static void main(String[]args){
int[] array=new int[10];
Thread t1=new ProvaThread(1).start();
Thread t2=new ProvaThread(2).start();//codice non corretto ma rende l' idea dello start
array[x]=10;
.
.
.riempimento dell' array
t1.setArray(array);
t2.setArray(array);
}}


In questo caso una stampa, da specifiche, garantisce uno 0, giusto?

PGI-Bis
14-10-2009, 16:38
Direi che è corretto, anche se il secondo caso potrei non averlo capito tanto bene.

Il primo è correttamente sincronizzato per via dello "start": ciò che è stato fatto prima dello start è visibile nel run del thread.

Se intendo correttamente il secondo caso, anche qui la tua deduzione è corretta. Il main cambia un valore dell'array dopo lo start dei thread. C'è ancora il punto di sincronizzazione nello start solo che stavolta c'è anche una scrittura successiva a quel punto a fronte della quale lo "start" non è più rilevante.

Al che sorge l'interrogatorio: se volessimo obbligare i due thread a vedere quel valore 10 per il componente x di array, come potremmo fare?

nuovoUtente86
14-10-2009, 16:48
Una soluzione l' avevi proposta qualche post fa: rendere volatile array e fare un assegnamento array=array. Oppure usare qualche meccanismo esplicito di locking.

Nel primo blocco di codice postato,non mi è chiarissimo cosa avviene se i thread invece di una stampa cambiano qualche cella dell' array.

banryu79
14-10-2009, 17:17
Una soluzione l' avevi proposta qualche post fa: rendere volatile array e fare un assegnamento array=array. Oppure usare qualche meccanismo esplicito di locking.
Ancora più banale: spostare quella istruzione prima della creazione (e successivo start) dei due thread :Prrr:
(Scherzo, sto seguendo con molto interesse il thread :) )

PGI-Bis
14-10-2009, 19:21
Nel primo blocco di codice postato,non mi è chiarissimo cosa avviene se i thread invece di una stampa cambiano qualche cella dell' array.

Be', vediamo.

Main-Thread: crea l'array e lo inizializza con dei valori. Questa inizializzazione comporta la scrittura di valori sull'heap
Main-Thread: avvia Thread 1. L'avvio è sincronizzato con le azioni precedenti che, quindi, happen-before la prima azione di Thread 1
Main-Thread: avvia Thread 2. Come sopra.

Thread 1: legge un campo - un componente dell'array. E' un'azione inter-thread. Abbiamo visto che è sincronizzata con la scrittura di Main-Thread.
Thread 2: legge un campo, vedi sopra.


Ora supponiamo che Thread 1 scriva su un componente dell'array.

Thread 1: array[3] = 20;

Naturalmente dal punto di vista di Thread 1 non c'è nessuno problema. Se per caso Thread 1 dovesse successivamente leggere il valore di array[3] vedrà 20 perchè le azioni intra-thread - cioè le azioni compiute all'interno di uno stesso thread - hanno l'effetto che dovrebbero avere secondo l'ordine di apparizione nel codice.

Ma cosa succederebbe se in seguito Thread 2 leggesse il valore di array[3]? Abbiamo la garanzia che veda 20? O potrebbe vedere 10?

Per stabilire se sia certo che Thread 2 veda 20 dobbiamo poter rinvenire nel codice una relazione happens-before tra la scrittura:

Thread 1: array[3] = 20;

e la successiva ipotetica lettura:

Thread 2: print(array[3]);

Questa relazione tuttavia non c'è.

C'è una relazione tra ciò che accade prima di start nel Thread Main e ciò che accade dopo start in Thread 1 e Thread 2 ma non c'è tra ciò che capita in Thread 1 e 2 e quello che Main fa dopo averli avviati.

La mancanza di questa relazione ci fa dire che il programma non è correttamente sincronizzato e il risultato è indeterminato. Nella fattispecie sono legittime sia la lettura del valore 10, sia la lettura del valore 20.

Non è invece ammesso che array risulti null o che i suoi componenti valgano zero.

nuovoUtente86
14-10-2009, 21:44
Era quello che mi aspettavo infatti.
Main-Thread: crea l'array e lo inizializza con dei valori. Questa inizializzazione comporta la scrittura di valori sull'heap

anche il Main, essendo di fatto un thread, deve rispondere al modello dell' area privata di lavoro(che a quanto ho capito è solo un qualcosa di teorico)? o girando la domanda se l' array fosse stato istanziando da un altro thread e non dal main, senza sincronizzazione sarebbe finito nella zona globale subito o nelle variabili private?

PGI-Bis
14-10-2009, 22:08
I thread java sono tutti uguali. Il thread "main" non fa eccezione.

Tieni conto che in Java il metodo è l'unità di esecuzione dei Thread, cioè quando vedi un metodo sai che c'è un thread che lo sta eseguendo. Il metodo main è un metodo, quindi le istruzioni che contiene sono eseguite da un Thread.

la necessità di sincronizzare discende da questo fatto. Quando nel metodo main troviamo:

public static void main(String[] args) {
int[] array = {10, 10, 10, 10, 10};

Sappiamo che quella riga è eseguita da un certo Thread (la cui identità otterremmo invocando Thread.currentThread()).

Dopo c'è un:

new ProvaThread(array, 1).start();

quindi sappiamo che un Thread sicuramente diverso da quello che ha eseguito la prima riga farà qualcosa su array.

Ti confermo che il concetto dell'area privata di lavoro è solo metaforico: rende l'idea del perchè sarebbe possibile che un thread non veda un certo valore che nel codice sembra invece essere chiaramente determinato.

nuovoUtente86
14-10-2009, 22:36
Ma l' effettiva sincronizzazione avviene appena prima di start(), indipendentemente che poi il thread operi o meno sull' array(starto un thread che stampa ciao e poi muore), oppure è legata al fatto che il thread sicuramente andrà ad operarci su?


Ti confermo che il concetto dell'area privata di lavoro è solo metaforico: rende l'idea del perchè sarebbe possibile che un thread non veda un certo valore che nel codice sembra invece essere chiaramente determinato.
a livello pratico come è gestita la cosa dalla jvm?

PGI-Bis
14-10-2009, 22:53
La sincronizzazione viaggia sempre in coppia: servono due azioni una compiuta da un Thread e una compiuta da un'altro Thread.

Ad esempio quando un Thread esce da un blocco synchronized, cioè che ha fatto fino ad allora è visibile al Thread che successicavamente entri in un blocco sincronizzato sullo stesso monitor.

Non c'è, per intenderci, una regola che dica: ok, dopo aver fatto questo il Thread aggiorna la metaforica regione di memoria condivisa. No, è sempre una questione di rapporto tra almeno due thread: dopo aver fatto questo, ciò che è stato fatto prima sarà visibile a qualcun'altro.

E' importante tenerlo presente altrimenti uno potrebbe pensare, come pare qualcuno faccia, che basti un punto di sincronizzazione non condiviso a rendere visibile l'operato di un thread:

Thread 1: scrivi, leggi, modifica e trallallà
Thread 1: synchronized(X) {} //PAF, il Thread "scarica" nell'HEAP

NO, non funziona così. Vale solo nei confronti di un secondo Thread per cui esista un vincolo hb col primo.

Quanto all'implementazione, non ho la più pallida idea di come sia fatta. Sì, in generale ci sono queste istruzioni nell'architettura di una CPU che sembrano essere in grado di stabilire se un valore debba essere preso presumo dalla cache piuttosto che dalla memoria principale. Poi si narra che ogni thread abbia la sua pila di "frame", uno per ogni metodo in esecuzione, che sono delle strutture dati che contengono una copia delle variabili chiamate in causa dall'esecuzione di un metodo.

Ma se vieni a chiedere dove sia allocata la memoria usata da questi frame per me potrebbe benissimo stare tra Nettuno e Plutone.

Comunque ci sono i sorgenti della JVM nel progetto OpenJDK. Una paio di volte ho anche provato a guardarli ma, francamente, più che capirci qualcosa direi di essere rimasto lì in affascinata contemplazione.

nuovoUtente86
14-10-2009, 23:14
Provo a rispondermi da solo: in questo caso
main:{
int []array={1,2,2,......};
new Thread{public run(){System.out.println("ciao");}}.start();

esiste un HB, come abbiamo assodato, tra l' ultima istruzione del lanciante e la prima del lanciato, quindi array dovrebbe essere scaricato.


Comunque ci sono i sorgenti della JVM nel progetto OpenJDK. Una paio di volte ho anche provato a guardarli ma, francamente, più che capirci qualcosa direi di essere rimasto lì in affascinata contemplazione.

la jvm dovrebbe essere quasi totalmente scritta in c++

PGI-Bis
14-10-2009, 23:22
La JVM HotSpot di Sun è scritta in C, C++, Java e Assembly.

So che usa assembly per usare lo stack del processo java anzichè creare una seconda pila per le istruzioni della jvm, cosa che pare non sia possibile fare in C o C++.

C'è anche una JVM scritta in Java, sempre di Sun ma adesso non trovo più il link.

Comunque a me basta che ci sia :).

nuovoUtente86
14-10-2009, 23:40
Non avendo ricevuto risposta negativa riguardo la mia ultima affermazione sull' HB presumo sia corretta:D

C'è anche una JVM scritta in Java, sempre di Sun ma adesso non trovo più il link.
credo sia compilata in maniera nativa però.
Ormai del resto si fanno anche sistemi operativi in Java come JNode. non so se tu lo abbia provato. Io ho avuto la tentazione ma lo vedo un progetto troppo immaturo per poter attecchire, considerando già le poche percentuali di mercato che hanno Apple e linux.

PGI-Bis
15-10-2009, 12:02
Di fronte a quel codice non è necessario che siano emesse particolari istruzioni di sincronizzazione da parte della JVM perchè l'array non è usato dal nuovo Thread.

Non c'è una relazione happens before perchè non c'è la coppia di azioni (uno legge, l'altro scrive o uno scrive e l'altro legge) richieste per la sincronizzazione.

Se il nuovo Thread usasse l'array allora la JVM sarebbe tenuta a rendere visibili le modifiche che il main apporta all'array al nuovo Thread.

nuovoUtente86
15-10-2009, 12:19
a Ok perfetto,
Se il thread startato fa come prima istruzione
stampa (array[x])...è piuttosto immediato "vedere"l' HB
ma se facesse alla 1000esima istruzione

if(succede qualcosa){stampa (array[x])}
altrimenti esci
la sincronizzazione dovrebbe avvenire cmq.
Quindi sostanzialmente viene analizzato il codice(detto in maniera rude) del secondo thread per stabilire se c'è una sincronizzazione da operare?

PGI-Bis
15-10-2009, 12:25
Esatto.

nuovoUtente86
15-10-2009, 12:46
Presumo che l' analisi avvenga a compile-time anche se un blocco del genere ad esempio, suggerirebbe il contrario:

main{
array[]
...
....
if(qualcosa){new Thread(array).start()//thread che fa qualcosa su array}
else stampa("ciao");
}

PGI-Bis
15-10-2009, 13:15
Non so quando avvenga. Presumo durante la compilazione statica (da java a .class). Nel caso che proponi non sono richiesti particolari dinamismi: nel primo ramo dell'if ci sarà un'istruzione di sincronizzazione, nel secondo no.