View Full Version : [JAVA] Multithreading & Swing
SubSeven
23-06-2014, 22:57
"Meglio un morto in casa, che una professoressa inaffidabile sull'uscio"
Tra le sensazioni più belle che la vita universitaria mi sta facendo sperimentare, sicuramente rientra il fatto di dover sostenere prove d'esame che contengono argomenti mai trattati a lezione. Non sarò il primo, ma nemmeno l'ultimo :D
Veniamo a noi.
Per l'esame di Programmazione Orientata agli Oggetti, avrò da sviluppare un programmino in Java (Netbeans) all'interno del quale dovrò gestire la mutua esclusione tra più thread che condividono una struttura dati in comune.
Secondo il testo: "L'applicativo da implementare consiste di un main che istanzia 4 thread di gestione (T1, T2, T3, T4) [...] Ciascun Thread dispone di una propria GUI che:
• espone un bottone: “inserisci” che se premuto causa l'inserimento di tale utente all'interno del Database (tale operazione di produzione deve essere implementata in maniera esclusiva rispetto agli altri Thread e in coerenza con
l'algoritmo dei produttori/consumatori)
• espone un bottone “cancella” che se premuto causa l'estrazione (e quindi cancellazione) del medesimo utente dal Database (tale operazione di consumo deve essere implementata in maniera esclusiva rispetto agli altri Thread e
in coerenza con l'algoritmo dei produttori/consumatori)"
Creata la GUI (un JFrame), dal main istanzio 4 Thread e li eseguo:
Thread T1 = new Thread (new GUI(db));
T1.start();
Thread T2 = new Thread (new GUI(db));
T2.start();
Thread T3 = new Thread (new GUI(db));
T3.start();
Thread T4 = new Thread (new GUI(db));
T4.start();
Quello che ottengo è la visualizzazione di 4 finestre diverse, tutte con la stessa GUI.
Ogni finestra ha dei campi di testo per l'inserimento di dati, un pulsante INSERISCI (per aggiungere un elemento alla struttura dati) e un pulsante RIMUOVI (per rimuoverlo, appunto).
Sia l'inserimento, sia la rimozione mettono in Sleep (per 2 secondi) il thread da cui è stata lanciata l'azione.
Il corretto svolgimento dell'esercizio prevede che, una volta iniziata l'operazione con il thread 1, cliccando sul pulsante del thread 2, l'azione di quest'ultimo non parta prima del completamento di quella iniziata nel thread 1.
Qual è il problema?
Dal momento che faccio eseguire il codice per l'inserimento/cancellazione di un elemento dalla struttura dati direttamente dal bottone della GUI, visto che quest'ultima azione viene affidata all'EDT (Event Dispatching Thread), mi si bloccano le UI di tutte e 4 le finestre aperte, non consentendomi di fare altro prima della fine del processo avviato.
La mia domanda è: come fare a far eseguire il codice su 4 thread veramente separati?
sottovento
24-06-2014, 06:43
Ti capisco. Sono passati (troppi) anni dalla laurea, ma ho ancora il rancore verso certi professori.
Veniamo a noi. Ovviamente occorrerebbe vedere il software che hai scritto, ma da quanto hai raccontato, hai ALMENO 6 thread che girano nel tuo sistema:
- il thread main
- il thread di Swing per la gestione dell'HMI
- T1, T2, T3 e T4
Non importa quante finestre decidi di aprire, saranno tutte gestite dal thread di cui sopra.
E' quindi evidente che se, nella callback (vale a dire nella actionPerformed() in risposta alla pressione di un bottone) metti un ritardo di 2 secondi, allora TUTTE le finestre saranno bloccate per due secondi, perche' il thread swing dovra' eseguire quella pausa e non avra' modo di rispondere agli altri eventi dell'HMI (pressione di altri bottoni, disegno delle finestre, ecc.).
La pausa deve essere fatta dai TUOI thread t1...t4, non dal thread (unico) dell'HMI.
Puoi postare qui il codice, se ti va. Per inciso: non e' che la prof. copia gli esempi direttamente dal libro alla lavagna? Mi e' capitato anche questo :D
SubSeven
24-06-2014, 10:54
Per inciso: non e' che la prof. copia gli esempi direttamente dal libro alla lavagna? Mi e' capitato anche questo :D
Guarda, la mia professoressa è così scarsa che per sbaglio ha proiettato, davanti 100 persone, un documento word con tutte le sue password... il tutto senza nemmeno accorgersene! :doh:
Comunque, posto il codice, così almeno rendo un'idea su come ho impostato l'applicativo :)
Main.java
public class Main {
public static void main (String[] args) {
ArrayList<Utente> l = new ArrayList<>();
Database db = new Database (l); // Passo la mia struttura dati ad un oggetto di tipo Database, nel quale sono contenuti tutti i metodi synchronized
Thread T1 = new Thread (new GUI(db));
T1.start();
Thread T2 = new Thread (new GUI(db));
T2.start();
Thread T3 = new Thread (new GUI(db));
T3.start();
Thread T4 = new Thread (new GUI(db));
T4.start();
}
Database.java - è la classe che contiene i metodi per l'inserimento/cancellazione di un nuovo elemento all'interno della struttura dati (per adesso la variabile booleana writeLock la sto usando solo per la funzione di inserimento; una volta raggiunto il risultato voluto estenderò la sua implementazione anche alla funzione cancellaUtente )
public class Database {
ArrayList<Utente> l;
boolean writeLock;
int readCount;
public Database(ArrayList<Utente> l) {
this.l = l;
writeLock = false;
readCount = 0;
}
private void writeLock() {
while (writeLock) {
try {
Thread.currentThread().wait();
} catch (InterruptedException ex) {
Logger.getLogger(Database.class.getName()).log(Level.SEVERE, null, ex);
}
}
this.writeLock = true;
}
private void writeUnlock() {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
Logger.getLogger(Database.class.getName()).log(Level.SEVERE, null, ex);
}
this.writeLock = false;
Thread.currentThread().notifyAll();
}
public synchronized void nuovoUtente(Utente nuovo) throws UtentePresenteEx {
writeLock();
if (l.isEmpty() || (verificaUtente(nuovo) == null)) {
l.add(nuovo);
} else {
throw new UtentePresenteEx(nuovo.getCodice_fiscale());
}
writeUnlock();
}
private synchronized Object verificaUtente(Utente nuovo) {
Iterator i = l.iterator();
while (i.hasNext()) {
Object ricerca = i.next();
if (nuovo.compareTo(ricerca) == 1) {
return ricerca;
}
}
return null;
}
public synchronized boolean cancellaUtente(Utente ricerca) throws UtenteNonPresenteEx {
Object risultato = verificaUtente(ricerca);
if (risultato != null) {
if (l.remove((Utente) risultato)) {
return true;
} else {
return false;
}
}
return false;
}
}
GUI.java (JFrame)
public class GUI extends javax.swing.JFrame implements Runnable {
Database db;
public GUI(Database db) {
initComponents();
this.db = db;
this.setTitle("Prova JAVA - "+ Thread.currentThread().getName());
}
private void inserisciBtnActionPerformed(java.awt.event.ActionEvent evt) {
inserisciBtn.setEnabled(false);
String nome = nomeTxt1.getText();
String cognome = cognomeTxt1.getText();
String codFisc = codFiscTxt.getText();
int eta = Integer.parseInt(etaTxt.getText());
String sesso = sessoTxt.getText();
String mail = mailTxt.getText();
Utente nuovo = new Utente(codFisc, nome, cognome, eta, sesso, mail);
try {
db.nuovoUtente(nuovo);
} catch (UtentePresenteEx ex) {
Logger.getLogger(GUI.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void cancellaBtnActionPerformed(java.awt.event.ActionEvent evt) {
String codice_fiscale = codFiscTxt.getText();
Utente ricerca = new Utente(codice_fiscale, null, null, 0, null, null);
try {
if (db.cancellaUtente(ricerca)) {
JOptionPane.showMessageDialog(null, "Utente rimosso con successo", "Informazione", JOptionPane.INFORMATION_MESSAGE);
deletedUsersTxt.append(ricerca.getCodice_fiscale()+"\n");
} else throw new UtenteNonPresenteEx(ricerca.getCodice_fiscale());
} catch (UtenteNonPresenteEx ex) {
JOptionPane.showMessageDialog(null, ex.getMessage(), "Errore", JOptionPane.ERROR_MESSAGE);
}
}
@Override
public void run() {
setVisible(true);
}
}
Utente.java - nemmeno a postarla, è una banale classe con i campi (nome, cognome, codice_fiscale, eta, sesso, mail) e relativi Getter & Setter.
Allego, per sicurezza, anche un zip con tutto il progetto (https://dl.dropboxusercontent.com/u/67202403/Palestra.zip) :D
La pausa deve essere fatta dai TUOI thread t1...t4, non dal thread (unico) dell'HMI.
Ecco, dovrei fare proprio questo! Ma come?
sottovento
24-06-2014, 16:39
Scusa il ritardo, sono dovuto partire per le mie "ferie forzate" (ogni mese devo lasciare l'Arabia Saudita e passare qualche giorno in Bahrain per motivi di visto).
Scarichero' la traccia che hai lasciato. Ad ogni modo, i tuoi thread non fanno granche', visto che lavorano solo per qualche microsecondo: rendono la GUI visibile e basta, poi muoiono. Quindi i thread T1...T4 praticamente NON esistono!
Ora ho un po' di cose da fare, poi leggo la traccia. A presto
SubSeven
24-06-2014, 16:41
Scusa il ritardo, sono dovuto partire per le mie "ferie forzate" (ogni mese devo lasciare l'Arabia Saudita e passare qualche giorno in Bahrain per motivi di visto).
Scarichero' la traccia che hai lasciato. Ad ogni modo, i tuoi thread non fanno granche', visto che lavorano solo per qualche microsecondo: rendono la GUI visibile e basta, poi muoiono. Quindi i thread T1...T4 praticamente NON esistono!
Ora ho un po' di cose da fare, poi leggo la traccia. A presto
Ok, attendo allora :)
Io toglierei qui quattro thread che come ha già detto sottovento hanno vita breve. Piuttosto crea un ExecutorService e passalo alla gui così può usarlo per eseguire dei Runnable contenenti la chiamata al database e la sleep in un thread separato senza bloccare l'interfaccia.
Ti lascio un paio di link alla documentazione per partire
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Callable.html
sottovento
25-06-2014, 01:56
VICIUS, hai perfettamente ragione ma questo e' un esercizio e da quanto ho capito i 4 thread sono richiesti. Occorre provare a dar loro un senso :D
Ho dato un'occhiata veloce al codice. Per prima cosa, devo dire che mi piace il tuo stile, sei molto pulito. Quindi le modifiche non saranno difficili da eseguire.
La prima modifica da fare riguarda entrambi i metodi inserisciBtnActionPerformed() e cancellaBtnActionPerformed().
Questa e' la tua implementazione:
private void inserisciBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_inserisciBtnActionPerformed
inserisciBtn.setEnabled(false);
String nome = nomeTxt1.getText();
String cognome = cognomeTxt1.getText();
String codFisc = codFiscTxt.getText();
int eta = Integer.parseInt(etaTxt.getText());
String sesso = sessoTxt.getText();
String mail = mailTxt.getText();
Utente nuovo = new Utente(codFisc, nome, cognome, eta, sesso, mail);
try {
db.nuovoUtente(nuovo);
} catch (UtentePresenteEx ex) {
Logger.getLogger(GUI.class.getName()).log(Level.SEVERE, null, ex);
}
}//GEN-LAST:event_inserisciBtnActionPerformed
private void cancellaBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancellaBtnActionPerformed
String codice_fiscale = codFiscTxt.getText();
Utente ricerca = new Utente(codice_fiscale, null, null, 0, null, null);
try {
if (db.cancellaUtente(ricerca)) {
JOptionPane.showMessageDialog(null, "Utente rimosso con successo", "Informazione", JOptionPane.INFORMATION_MESSAGE);
deletedUsersTxt.append(ricerca.getCodice_fiscale()+"\n");
} else throw new UtenteNonPresenteEx(ricerca.getCodice_fiscale());
} catch (UtenteNonPresenteEx ex) {
JOptionPane.showMessageDialog(null, ex.getMessage(), "Errore", JOptionPane.ERROR_MESSAGE);
}
}//GEN-LAST:event_cancellaBtnActionPerformed
La prima istruzione
inserisciBtn.setEnabled(false);
disabilita il bottone, ma non c'e' un codice complementare che lo riattiva. Se il problema e' quello di farlo riattivare dopo 2 secondi, puoi risolverlo modificando direttamente all'interno dei due metodi che hai scritto (ATTENZIONE - quanto scrivo e' java 8):
javax.swing.Timer timer = new javax.swing.Timer(2000, event -> inserisciBtn.setEnabled(true));
timer.setRepeats(false);
timer.start();
Alla scadenza del timer, il bottone verra' riattivato.
Il secondo problema e' che l'inserimento/cancellazione viene fatto direttamente da questi due metodi. Devi togliere l'inserimento/cancellazione da li', metterli nei thread T1...T4 ed inventarti un modo per far comunicare i due thread. Secondo me il modo piu' semplice (di cui spesso abuso) e' utilizzare le ArrayBlockingQueue<>. In pratica, Tx e la corrispondente coda si parlano attraverso questa coda: la gui inserisce l'oggetto e il thread tx lo legge ed esegue l'operazione. Semplice, no?
Facendo cosi' dai un senso ai thread e non blocchi l'HMI.
Infine: probabilmente ti e' scappato il fatto che il tuo codice genera un
java.lang.IllegalMonitorStateException
poiche' l'istruzione
Thread.currentThread().notifyAll();
e' fuori da qualsiasi blocco sincronizzato, pertanto l'istruzione non e' valida.
Fai attenzione: questo e' il motivo che ti faceva sembrare che l'HMI fosse incartata (insieme ovviamente alla disabilitazione del bottone). Se avessi messo questa istruzione al posto giusto, probabilmente non ti saresti accorto di nulla e non ti saresti chiesto se effettivamente i thread stavano girando correttamente. In pratica, l'errore ti ha dato la possibilita' di "vedere" esattamente le cose come stanno.
Riassumento, i primi passi secondo me sono:
1 - riabilitare il bottone
2 - fare in modo the i thread T1...T4 abbiano un senso, cioe' che dentro il metodo run() ci sia del codice, ed il codice deve essere un ciclo (infinito) che legge dalla coda bloccante che arriva da HMI. Una volta letto il dato (ed un codice operativo che specifica cosa farne), inserisce o cancella il dato;
3 - ripensare lock dei thread, vale a dire la parte che ti fallisce. Pero' io affronterei prima i punti 1 e 2. Per fare questa parte, lascerei stare i monitor ed utilizzerei l'oggetto Semaphore, sempre che la prima soluzione non ti sia stata imposta...
tienici aggiornati
vBulletin® v3.6.4, Copyright ©2000-2025, Jelsoft Enterprises Ltd.