18. Programmazione concorrente
Andrea Pastore 19/03/2020 0
Prima di introdurre questo agomento dobbiamo capire come funzionano i moderni processori: vediamo la foto di un’architettura di un processore intel:
Vediamo che è composto da varie parti: i core (le unità che di elaborazione, il codice che scriviamo viene elaborato da loro) una piccola memoria interna (da non confondersi con la memoria RAM, che non risiede nel processore) e poi una scheda video. Il numero di processi che può essere eseguito contemporaneamente dipende dal numero di core, quindi il processore mostrato in figura può eseguire quattro processi contemporaneamente. Il sistema operativo ha il compito di assegnare le risorse ad un processo: tra le altre cose deve decidere a quale processore assegnare il processo, controllare che non si blocchi, aggiornare la coda dei processi in attesa di essere eseguiti e molto altro ancora. Un processo resta in esecuzione per una frazione di secondo, poi se ha esaurito il compito viene terminato, altrimenti viene reinserito nella coda dei processi che in attesa di essere eseguiti.
I programmi moderni, a meno che non siano programmi molto piccoli sono composti da tanti processi in esecuzione sul proprio computer, ognuno con un compito. Un esempio è rappresentato dai browser web, come Firefox: l’architettura di questo browser prevede attualmente che ci sia un processo deputato a mostrare l’interfaccia (che mostra ad esempio le tab aperte) e altri processi che gestiscono le tab.
Lo scheduler del sistema operativo
Per stabilire quale processo debba essere eseguito ogni sistema operativo dispone di un software chiamato scheduler, che gestisce la coda dei processi tenendo conto di una serie di fattori: priorità, stato del processo e molti altri. Se il sistema operativo è progettato per gestire hardware in grado di gestire più thread contemporaneamente (ovvero tutti gli strumenti che utilizziamo maggiormente oggi: computer, telefonini, ma anche centraline di oggetti di uso comune come l’automobile) il suo ruolo si complica perché deve tenere conto anche degli altri thread. Ad esempio un thread di un processo potrebbe dover aspettare che un altro thread termini l’esecuzione.
La programmazione concorrente in Java
Java mette a disposizione due modi per creare processi concorrenti: la prima possibilità consiste nella creazione di una classe che estende la classe Thread, la seconda consiste nella creazione di una classe che implementa l’interfaccia Runnable. Le differenze dal punto di vista del codice sono minime, creiamo due classe Trhead_A e Trhead_B per vederle:
Classe che estende la classe Thread
public class Thread_A extends Thread {
private int contatore;
public Thread_A() {
this.contatore = 0;
}
public void run() {
this.contatore ++;
System.out.println("Trhead A: contatore= "+contatore);
}
}
Classe che implementa l’interfaccia Runnable
public class Thread_B implements Runnable{
private int contatore;
public Thread_B() {
this.contatore = 0;
}
@Override
public void run() {
this.contatore++;
System.out.println("Thread B: contatore="+contatore);
}
}
Notiamo che entrambe le classi hanno un metodo public void run(). Questo è un metodo necessario in quanto descrive le operazioni che deve fare il Thread. Nei nostri esempi qui sopra i thread incrementano un contatore e lo stampano.
Lanciare un Thread
A seconda del metodo che abbiamo scelto, i Thread si lanciano in due modi diversi. Se abbiamo scelto di creare le classi che estendono la classe Thread, come nel nostro esempio del Trhead_A, scriveremo:
Thread_A t = new Thread_A();
t.start();
Se usiamo una classe che estende l’interfaccia Runnable creeremo prima un’istanza di quella classe, e un Trhead a partire da quella:
Thread_B rc = new Thread_B();
Thread t1 = new Thread(rc);
t1.start();
Nota: in entrambi i casi per far partire un thread bisogna invocare il metodo start().
Assegnare la priorità ad un thread
È possibile impostare la priorità di un thread con il metodo setPriority. Java prevede per la priorità un valore da 1 a 10, se non lo impostiamo manualmente la priorità assegnata di default è 5. La classe Trhead dispone di tre costanti che sono:
MAX_PRIORITY = 10
MIN_PRIORITY = 1
MEDIUM_PRIORITY = 5
Se ad esempio voglio impostare la priorità massima scriverò:
t.setPriority(Thread.MAX_PRIORITY);
Per impostare altri valori sulla scala da 1 a 10 possiamo usare i numeri.
Mettere in pausa un thread
Può capitare di dover fermare un thread per un certo intervallo di tempo (ad esempio perché deve ripetere una determinata operazione dopo un tempo prestabilito), per questo Java mette a disposizione diversi metodi. Sleep, join e yeld.
sleep
prende in input il numero di millisecondi durante i quali il thread deve stare in pausa. Per invocarlo correttamente bisogna utilizzare il costrutto try .. catch, perché dobbiamo prevenire il fatto che un altro thread provi a chiamare il thread dove abbiamo invocato il metodo sleep:
try {
t1.sleep(10);
} catch (InterruptedException ex) {
// handle the error
System.out.println("si è verificata un'eccezione");
}
join
questo metodo serve a mettere in attesa il thread su cui è invocato fino a quando altri trhead al suo interno non abbiano terminato l’esecuzione. Supponiamo di avere un thread t1 che crea un thread t2 per eseguire un’operazione. L’istruzione:
t2.sleep(10);
metterà in attesa il thread t1 fino a quando t2 non avrà terminato la sua esecuzione.
yeld
È un metodo statico che suggerisce allo scheduler di mettere in pausa il tread corrente per lasciare spazio ad altri. Viene invocato in questo modo:
Thread.yield();
Potrebbero interessarti anche...
Andrea Pastore 19/03/2020
2. Ambienti di sviluppo
Per ambiente di sviluppo (in inglese IDE – Integrated Development Environment) si intende un software in grado di aiutarci con la scrittura del codice, segnalando errori di sintassi, suggerire il codice che stiamo scrivendo (le funzioni di autocompletamento) e molti altri tool avanzati. Vediamo i principali:
Eclipse
Ambiente di sviluppo polivalente sviluppato dalla Eclipse Foundation nel 2004. È uno degli Ide più popolari, non solo per Java, ma anche per il C, php e altro ancora.
Netbeans
Ambiente di sviluppo sviluppato da Sun e per i primi tempi ide di riferimento per Java. Quando java è passato ad Oracle ha perso popolarità in favore di Eclipse, recentemente è stato donato alla Apache Foundation.
In questo corso utilizzeremo Eclipse, diamogli quindi un’occhiata più da vicino e vediamo come creare un nuovo progetto, un nuovo package e una nuova classe.
Creazione di un progetto in Eclipse
Dal menu file clicco su new e poi su Java Project. Si aprirà una schermata come quella sottostante:
Dobbiamo semplicemente scegliere il nome del nostro progetto ignorando per ora le altre voci, che consentono delle personalizzazioni avanzate che in questa fase non ci interessano.
Creare un package in Eclipse
Sulla sinistra Eclipse visualizza una barra chiamata Package Explorer, con tutti i progetti. Per creare un nuovo package clicchiamo col tasto destro sul nome del progetto, scegliamo new e poi selezioniamo package. Si aprirà questa schermata:
inseriamo il nome del nostro package. Esiste una convenzione per cui il nome di un package comincia sempre con la lettera minuscola, è bene sempre rispettare questo tipo di convenzioni.
Creare una classe in Eclipse
Per creare una classe clicchiamo con il tasto destro sul nome del package che la deve contenere, clicchiamo su new e scegliamo class. Apparirà la seguente schermata:
Nel campo name possiamo inserire il nome della classe, che in questo caso deve iniziare con una lettera maiuscola. Questa schermata contiene varie opzioni che utilizzeremo più avanti, per ora ci concentriamo su una opzione, quella che consente di inserire il metodo main. Ogni volta che vogliamo creare una classe main (che ricordiamo è la classe che fa avviare il progetto) dobbiamo spunare la voce
public static void main(String[] args)
le prime classi che creeremo avranno tutte il metodo main
Scorciatoie di tastiera di Eclipse
Eclipse come tutti gli ide mette a disposizione diverse scorciatoie di tastiera, la più usata è data dalla combinazione:
ctrl + spazio
che serve ad attivare l’autocompletamento. I comandi in Java possono essere molto lunghi, si prenda ad esempio questo comando che vedremo a breve:
System.out.println("Hello world!")
Scrivendo solo la parola
System.out.pr
e poi usando ctrl + spazio Eclipse ci darà le opzioni per l’autocompletamento.
Hello world!
È tradizione iniziare a programmare scrivendo un programma che stampi la scritta:
Hello world!
Il codice di questo programma è il seguente
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
Come vediamo questa è una classe con il metodo main, ovvero è una classe che può essere lanciata in esecuzione. Nel metodo main vediamo una riga di codice:
System.out.println("Hello world!")
Il comando System.out.println serve per stampare: in questo caso prende in input la stringa Hello world! Quando dobbiamo scrivere questo comando possiamo usare una scorciatoia di Eclipse, scriviamo solo la parola:
syso
e poi premiamo ctrl + spazio. Eclipse trasformerà quelle quattro lettere in:
System.out.println()
a quel punto dovremmo solo indicare a Java cosa stampare all’interno delle parentesi tonde.
Andrea Pastore 19/03/2020
14. I file in Java
Java mette a disposizione diverse classi per operare con i file. La prima classe che vediamo è la classe File, che consente di prendere informazioni sul file che vogliamo utilizzare, ad esempio per sapere se il file esiste, se possiamo scrivere e molto altro ancora. Un oggetto file si inizializza cosi:
File f = new File("prova.txt");
in questo caso abbiamo usato solo il nome del file, significa che Java cercherà quel file nella cartella dove sta eseguendo. Se il file che desideriamo si trova in un altra cartella possiamo anche fornire un percorso assoluto, come in questo caso:
File f = new File("/nomecartella/sottocartella/prova.txt");
Nota: per utilizzare questa classe dobbiamo importarla scrivendo:
import java.io.File;
Metodi della classe file
Vediamo quali sono i principali metodi della classe File:
exists() | Restituisce true se il file esiste, false altrimenti |
createNewFile() | Crea il file. Bisogna gestire un’eccezione di tipo IOException che viene lanciata nel caso il file non possa essere creato |
canRead() | Restiuisce true se Java può leggere il file |
canWrite() | Restituisce true se Java può scrivere il file |
isHidden() | Restituisce true se il file è nascosto, false altrimenti |
isFile() | Restituisce true se il percorso indicato è effettivamente un file, false altrimenti |
isDirectory() | Restituisce true se il percorso è una cartella, false altrimenti |
length() | Restituisce la dimensione del file in byte (sigifica che per avere ad esempio i mb devi dividere il valore restituito da long per 1000000 |
getAbsolutePath() | Stampa il percorso completo del file es C:/cartella/cartella... ecc |
astModified(); | Restituisce la data dell'ultima modifica in millisecondi dal 1970 (vedi il file helloDate per convertirli in una data tradizionale) |
Scrivere su un file
Per scrivere su un file dobbiamo utilizzare la classe la classe FileOutputStream, il cui costruttore prende in input un oggetto di tipo file. La scrittura nel file avviene con il metodo write: questo metodo prende in input una sequenza di byte, quindi dobbiamo convertire la stringa in byte con un metodo apposito: getBytes().
Nota: la creazione dell'oggetto FileOutputStream deve essere gestita con il costrutto try / catch per gestre due tipi di eccezioni: FileNotFoundEsception (che può verificarsi se scriviamo un percorso di file errato) e IOException (che può verificarsi quando Java non riesce a scrivere nel file).
public static void main(String[] args) {
File f = new File("prova.txt");
FileOutputStream out = null; // lo inizializziamo a null perché va gestito nel try catch
try {
out = new FileOutputStream(f);
out.write("Ciao".getBytes());
System.out.println("Scrittura eseguita con successo!");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}
}
La modalità append
Ci sono casi in cui il file contiene già delle informazioni e non vogliamo perderle. Possiamo usare la modalità append, che aggiunge il testo alla fine del file. Per usare questa modalità al momento della creazione dell’oggetto FileInputStream dobbiamo specificare un secondo parametro, che riguarda appunto se usare o meno la modalità append. La stringa:
out = new FileOutputStream(f);
diventa
out = new FileOutputStream(f,true);
Leggere da un file
come per FileOutputStream anche la classe FileInputStream ha bisogno di un oggetto di tipo file, e la creazione dell'oggetto FileInputStream deve essere gestita con il costrutto try / catch per gestre due tipi di eccezioni: la prima è FileNoTfoundEsception che può esere generata dal costruttore e la seconda è IOException generato dal metodo read che si occupa di leggere materialmetne il file.
public static void main(String[] args) {
File f = new File("prova.txt");
FileInputStream in = null;
try {
in = new FileInputStream(f);
/*
* scriviamo ora il codice per la lettura del file: il seguente ciclo while legge il
* codice carattere per carattere, si fermerà solo quando il metodo read nel while ha
* dato risultato -1, che indica appunto la fine del file
*/
int i = 0;
while((i= in.read()) != -1) {
// read restuisce dei numeri interi che noi dobbiamo convertire in formato char (ovvero il
// singolo carattere, per fare questo usiamo un cast
// (char) i; è un cast, ovvero convertiamo la variabile i ad un valore diverso.
// Nota: non è sempre possibile fare i cast
char c = (char) i;
System.out.print(c);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Nota: i metodi che abbiamo visto consentono di leggere file semplici (txt, csv oppure formati definiti da noi). Per leggere formati più elaborati, come ad esempio i file Excel, dobbiamo usare librerie apposite.
Andrea Pastore 19/03/2020
17. L’operatore Lambda
L’operatore lambda è stato introdotto in Java 8 ed è rappresentato dal simbolo -> e consente di velocizzare alcune operazioni. Supponiamo di avere il seguente codice:
ArrayList<String> stringhe = new ArrayList<>();
stringhe.add("Andrea");
stringhe.add("Antonio");
stringhe.add("Alessandro");
stringhe.add("Alfonso");
supponiamo ora di voler stampare tutti i nomi presenti in questo ArrayList: se volessimo usare un metodo tradizionale potremmo scrivere
for(int i = 0; i<4; i++) {
System.out.println(stringhe.get(i));
}
vediamo ora come fare la stessa cosa con l’operatore lambda:
stringhe.forEach((String s) -> System.out.println(s));
come vediamo è molto più sintetico: sul nostro array invochiamo il metodo forEach che applica l’espressione lambda a tutto l’array.
Con gli operatori lambda possiamo anche applicare dei filtri. Ad esempio supponiamo di voler stampare solo le stringhe che hanno valore Andrea. In modo tradizionale sarebbe:
for(int i = 0; i<4; i++) {
if(stringhe.equals("Andrea")) {
System.out.println(stringhe.get(i));
}
}
vediamo invece con l'operatore lambda:
stringhe.stream().filter(s->s.equals("Andrea")).forEach((s) -> System.out.println(s));
Abbiamo applicato il metodo stream per prendere tutti gli oggetti e poi il metodo filter per inserire il filtro che ci interessava, infine abbiamo usato di nuovo il metodo forEach per stampare.