<div class="page">
<div class="cover text-center">
<img class="mx-auto" src=/itb/images/logo_mislata.png alt="logo">
# Sincronització de fils
<div class="text-end fit-content ms-auto my-3 mt-auto pt-3">
<p><strong>Autor:</strong> Joan Puigcerver Ibáñez</p>
<p><strong>Correu electrònic:</strong> j.puigcerveribanez@edu.gva.es</p>
<p><strong>Curs:</strong> 2024/2025</p>
</div>
<div>
<p class="fw-bold mb-0">Llicència: BY-NC-SA</p>
<p class="d-none d-md-block">(Reconeixement - No Comercial - Compartir Igual)</p>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ca" target="_blank">
<img class="mx-auto" src="/itb/images/license.png" alt="Licence"/>
</a>
</div><!--license-->
</div><!--cover-->
</div><!--page-->
{:toc}
## Introducció
Els fils es poden comunicar intercanviant informació a través dels objectes
emmagatzemats a la memòria principal. Com que els fils pertanyen a un procés,
poden accedir tots als objectes del mateix que hi haja en memòria.
Aquest mètode de comunicació és molt eficient. Compartir la memòria pels
fils pot produir resultats erronis si els fils hi accedeixen alhora. Per
solucionar aquests problemes s'utilitza necessitem establir mecanismes
per sincronitzar i controlar l'accés als recursos.
## Problemes derivats de la concurrència
### Condició de carrera
__Condició de carrera__: es defineix així quan el resultat de l'execució
d'un programa depèn de lordre concret en què es realitzen els accessos a memòria.
_Exemple_: Suposem que tenim dos fils que realitzen una operació
sobre la mateixa variable `comptador`, que inicialment té el valor `10`.
- __Fil1__: `comptador++`;
- __Fil2__: `comptador--`;
En una execucó correcta, primer el __Fil1__ sumaria i despés el __Fil2__ restaria
o viceversa, però el resultat sempre seria el mateix.
Una __operació atòmica__ és aquella que en codi màquina només es tradueix
en una única instrucció. El problema és que __++__ i __--__ no són operacions
atòmiques i es tradueixen en codi màquina de la manera següent:
<div class="row">
<div class="col">
- __`i++`__:
```
registreX = comptador;
registreX = registreX + 1;
comptador = registreX;
```
</div>
<div class="col">
- __`i--`__:
```
registreX = comptador;
registreX = registreX - 1;
comptador = registreX;
```
</div>
</div>
Es podria donar el cas on aquestes instruccions s'executen
en el següent ordre:
```
Fil1: registre1 = comptador (registre1 = 10)
Fil1: registre1 = registre1 + 1 (registre1 = 11)
Fil2: registre2 = comptador (registre2 = 10)
Fil2: registre2 = registre2 - 1 (registre2 = 9)
Fil1: comptador = registre1 (comptador = 11)
Fil2: comptador = registre2 (comptador = 9)
```
Com es pot veure, el resulta és erroni.
### Inconsistència de memòria
La __inconsistència de memòria__ es produeix quan diferents fils tenen una
visió diferent del que hauria de ser la mateixa dada. Pot passar des del
no alliberament de dades obsoletes fins al desbordament d'un buffer («buffer overflow»).
_Exemple_: Suposem que tenim dos fils i la variable `comptador` que inicialment té
el valor 0.
- __Fil1__: `comptador++`;
- __Fil2__: `System.out.print(comptador)`;
Si les dues sentències s'executaren al mateix fil, primer la sumaria i
després s'imprimiria amb el valor 1.
Si aquestes instruccions s'executen en fils diferents no hi ha garantia
que primer es sume i que després s'imprimisca, de manera que el valor
imprès podria ser 0, cosa que és un resultat no desitjat.
### Inanició (starvation)
La __inanició__ és el fenomen que passa quan a un procés o fil se li
denega l'accés a un recurs compartit pel fet que altres processos o fils
sempre prenen en control d'aquest recurs abans per diferents motius.
Això es pot produir si un fil té una prioritat inferior a la resta de fils.
_Exemple_:
Imagina que estàs en la cua d'un mercat per comprar,
on es dóna prioritat a la gent més major per poder comprar abans.
En el moment que tú arribes, comencen a arribar molta gent més major
que tu tota la estona, sense parar.
Si no pots comprar menjar mai, en un temps estaràs famolenc.
En aquest exemple, tu eres un fil amb prioritat baixa i la gent major
son fils amb prioritat alta.
### Interbloqueig (deadlock)
L'__interbloqueig__ es produeix quan dos o més processos o fils es troben esperant
indefinidament per un esdeveniment que ha de generar un altre procés o fil que està
bloquejat o que es troba esperant un recurs que té assignat un altre procés o fil
que està bloquejat.
_Exemple_: Susposem que tenim 2 fils __F1__ i __F2__, i tenim dos
recursos (__R1__ i __R2__). Els dos fils necessiten tindre la propietat
dels dos recursos per poder treballar. La seqüencia d'instruccions de
cada fils és:
<div class="row">
<div class="col">
- __F1__:
```
Petició R1
Petició R2
Instruccions
Desbloquejar R2
Desbloquejar R1
```
</div>
<div class="col">
- __F2__:
```
Petició R2
Petició R1
Instruccions
Desbloquejar R1
Desbloquejar R2
```
</div>
</div>
Si els fils s'executen de manera paral·lela i executen la primera instrucció alhora
es bloquejarien entre ells, ja que cada fil està esperant a obtindre el recurs
que té en propietat l'altre fil, que no alliberarà fins que acabe.
{.center}{height=200}
### Bloqueig actiu (livelock)
El __bloqueig actiu__ és molt similar a un _interbloqueig_, però aquest cas,
el bloqueig es dóna perquè els fils involucrats van canviant el seu estat
per desbloquejar-se a la vegada, tornant a bloquejar-se mútuament.
Aquest bloqueig es dóna en algoritmes per previndre el _deadlock_.
_Exemple_: Imagineu-se que aneu per la vorera d'un carrer, i vegeu algú que
s'apropa en sentit contrari a tu. Els dos aneu pegats a la pared de les cases,
pensant que l'altra persona s'apartarà. En el moment que esteu molt a prop,
els dos s'aparteu cap al carrer, per deixar que l'altra persona passe.
Com els dos s'heu apartat, s'heu tornat a bloquejar entre vosaltres i decidiu a la vegada
tornar al lloc original. Si cap dels dos trenca el bucle, entreu en el que es coneix
com bloqueig actiu.
{.center}
## Mecanismes de sincronització
Els problemes anteriorment esmentats ocorren pel fet que diversos fils
s'executen concurrentment i no es té control sobre el seu ordre,
cosa que pot desembocar en una ordenació i uns resultats no dessitjats.
Per solucionar-ho, s'ha de controlar que els accessos a dades compartits
es realitzin de manera ordenada o __síncrona__. Quan estiguen executant codi
independent, que s'executin en paral·lel lliurement, s'anomena execució __asíncrona__.
### Condicions de Bernstein
Les condicions de Berstein defineixen les condicions que permeten
esbrinar si dos segments de codi ($S_1$ i $S_2$) es poden executar en
paral·lel de manera asíncrona en diferents fils.
Aquestes condicions permeten fer una anàlisi de dependència. Si les
dues instruccions són independents, es poden executar en paral·lel.
- __Dependència de flux__: Les variables d'entrada de $S_2$ han de ser
diferents de les variables d'eixida de $S_1$. En cas contrari, $S_2$ depén
de $S_1$.
_Example_:
$$
S_1: \textcolor{red}{C} = A + B \\
S_2: D = \textcolor{red}{C} + E
$$
- __Antidependència__: Les variables d'entrada de $S_1$ han de ser
diferents de les variables d'eixida de $S_2$. En cas contrari, $S_2$ depén
de $S_1$. És el cas contrari al punt anterior.
_Example_:
$$
S_1: D = \textcolor{red}{C} + E \\
S_2: \textcolor{red}{C} = A + B
$$
- __Dependència d'eixida__: Totes les variables d'eixida de $S_1$ han de ser
diferents de les variables d'eixida de $S_2$. En cas contrari els dos segments
podrien escriure al mateix lloc provocant resultats inesperats.
_Example_:
$$
S_1: \textcolor{red}{C} = A + B \\
S_2: \textcolor{red}{C} = D + E
$$
El codi que no complisca aquestes condicions s'ha d'executar de manera síncrona.
### Operacions atòmiques
Com s'ha exposat anteriorment, una __operació atòmica__ és aquella que es
tradueix com una única instrucció i s'executarà completament sense ser
interrompuda.
_Exemple_: Imagina que fas un Bizum de 10€ a un conegut. Aquesta operació
ha de restar 10€ del teu compte i transferir-los al compte de l'altra persona.
Aquesta operació d'ha de realitzar com una única instrucció o podrien
haver problemes de consistència en les dades.
### Secció crítica
Una __secció crítica__ és aquella en què s'accedeix a variables i recursos
compartits de manera ordenada, per la qual cosa les instruccions han
d'executar-se de manera síncrona. Aquest concepte es pot aplicar tant
a processos com a fils si accedeixen dades o recursos compartits.
- Si ja hi ha un procés o fil executant la secció crítica i un altre vol
executar-la, aquest últim es bloquejarà fins que el primer acabe
amb la secció crítica.
- D'aquesta manera s'estableix un abans-després en l'ordre d'execució
de seccions crítiques, així els canvis són visibles des de la
resta de processos o fils.
_Exemple_: Una secció crítica podria englobal les instruccions
per realitzar canvis en un compte bancari. Un fil ha d'esperar
a que acabe una transacció en un compte per poder fer canvis
en el mateix.
La secció crítica té un problema i aquest és dissenyar un protocol d'execució
de la secció crítica. Perquè estiga ben dissenyat ha de complir:
- __Exclusió mútua__. Si un procés o fil està executant la secció crítica,
cap altre pot passar a executar-la.
- __Progrés__. Si no hi ha cap procés o fil executant la secció crítica
i n'hi ha diversos esperant a executar-la, algun ha de començar.
No poden estar esperant tots si cap no és a la secció crítica.
- __Espera limitada__. Hi ha d'haver un nombre limitat de processos o
fils que entren a la secció crítica quan un procés o fil demana executar-la.
En cas contrari, es podria produir inanició.
Resumint, el codi corresponent a la secció crítica ha d'acabar en un temps
determinat i cal evitar la inanició. Per implementar la secció crítica hi ha
diferents mecanismes, la implementació dels quals depèn de cada SO.
Java proveeix aquests mecanismes, però ocultant al programador aquestes
diferències que hi ha a nivell de SO.
Els mecanismes són __objectes sincronitzats__, __semàfors__ i els __monitors__.
### Semàfors (semaphores)
Els __semàfors__ són mecanismes que permeten que un nombre limitat
de processos accedisquen de manera ordenada a un recurs. Es representa
amb una variable sencera, el valor de la qual és el nombre de llocs
disponibles al recurs compartit i una cua on emmagatzemar els processos
bloquejats que esperen l'accés.
A Java, s'utilitza la classe [`Semaphore`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html).
A la inicialització es proporciona un valor que serà el nombre de llocs
disponibles. Hi ha dues operacions atòmiques per accedir i modificar el
valor del semàfor:
- __[`adquire()`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html#acquire())__:
Mitjançant aquesta operació es disminueix el nombre de buits en un,
si el valor és menor que zero és que no hi ha lloc i el procés s'hauria
de bloquejar fins que hi haja lloc. En cas que el valor siga negatiu, indica
quans fils hi han en espera.
- __[`release()`](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html#release())__:
S'utilitza per avisar que un procés ha acabat d'utilitzar
el recurs. Amb aquesta operació s'augmenta un el nombre de llocs disponibles.
Si el valor és menor o igual a zero és que hi ha algun procés bloquejat
esperant i caldrà despertar-lo. El procés que es desperta és aleatori i
depèn de la implementació del semàfor i del SO on s'execute.
Els __MUTEX__ (**MUT**ual **EX**clusion) són semàfors binaris que sols
permeten que un fil tinga accés a un recurs. Podem crear un semàfor binari
amb `new Semaphore(1)`.
### Monitors
Un __monitor__ és un objecte o mòdul que es pot utilitzar d'una manera
segura per diversos fils i proporciona un mecanisme de exclusió mútua:
sols un fil pot accedir a aquest.
recurs. Es podria dir que els monitors són com un __forrellat (lock)__ que
permet l'accés o no al recurs.
El seu funcionament és molt similar al dels semàfors binaris,
però amb més simplicitat, ja que el programador no cal que faça
explicita l'adquisició i alliberament del monitor.
Els semàfors depenen que el programador introdueixi les
sentències a l'ordre correcte per no bloquejar el sistema.
A Java tots els objectes derivats de la classe __Object__(pràcticament tots),
tenen associats dos monitors: un afecta la pròpia classe i un altre que
afecta els mètodes definits com __`synchronized`__.
Per utilitzar un monitor a Java s'utilitza la paraula clau
`synchronized` sobre una regió del codi.
D'aquesta manera aquesta secció s'executarà com si fora una
__secció crítica__. `synchronized` es pot
utilitzar en mètodes i en blocs sincronitzats.
#### Mètodes sincronitzats
Un mètode declarat `synchronized` garanteix que no es puga executar de
manera concurrent a un altre mètode declarat `sinchronized` del mateix objecte.
Quan un fil invoca un mètode `synchronized` intenta prendre el «lock»
de l'objecte a què pertanya el mètode. Si està lliure l'adquireix i executa
el mètode. Si el «lock» està ocupat, el fil se suspendrà fins que s'allibere
el «lock».
En cas que el mètode siga `static synchronized`, el «lock» serà de la classe
i afectarà tots els mètodes `static synchronized` d'aquesta classe.
Per crear un mètode sincronitzat cal afegir la paraula `synchronized`
a la seua declaració. És important saber que els constructors per defecte
són síncrons, de manera que no es poden marcar amb `synchronized`.
- Quan un fil pren el «lock» d'un objecte pot accedir a un altre mètode
`synchronized` d'aquest objecte sense necessitat d'alliberar el «lock».
- Hi ha un «lock» per cada instància de l'objecte.
- Quan s'estenga (`extends`) una classe i se sobreescriga un mètode
`synchronized`, no és obligatori que el mètode nou siga synchronized.
Si es vol que el mètode de la subclasse siga sincronitzat, cal especificar-ho.
#### Blocs sincronitzats
Els mètodes sincronitzats utilitzen un monitor que afecta tot l'objecte
corresponent de manera que es bloquegen tots els mètodes sincronitzats.
Utilitzant aquest monitor, es podrien fer ús de mètodes no sincronitzats
mentre un fil tinga el «lock» dels mètodes sincronitzats de manera paral·lela.
Si volem adquirir el «lock» de tota la instància, hem d'utilitzar
un bloc `synchronized`.
```java
public static void main(String[] args){
...
synchronized(object){
...
}
}
```
#### wait() i notify()
Les eines de sincronització anteriors utilitzen els mètodes `wait()` i `notify()` internament
per realitzar la sincronització de fils.
Quan un fil executa el mètode `wait()`, quedarà en estat __suspés__ i pararà la seua execució
fins que niga notificat.
Quan un fil executa el mètode `notify()`, notificarà a un fil (el qual no es pot triar) que
pot continuar la seua execució. També existeix el mètode `notifyAll()`, que notificarà tots
els fils que estiguen esperant.
Tots aquests mètodes estan vinculats a un monitor en concret.
Aquests mètodes són útils si es vol esperar fins que una condició es complisca per poder
continuar amb l'execució del fil.
## Exemples en Java
### CopyShop
#### Explicació
Aquest programa simula una copisteria. La copisteria té 3 impresores
que es poden utilitzar al mateix temps. S'han creat 5 clients, cadascun
en un fil distint. Cada client realitza les següents accions:
- Mira si hi ha alguna impresora lliure.
- Si hi ha alguna, l'adquireix.
- Si no hi ha cap, espera.
- Quan aconsegueix accedir a una impresora lliure, imprimeix tots els documents.
- Quan acaba d'imprimir, allibera la impresora que estava utilitzant.
```java
shop.getSemaphore().acquire();
Printer printer = shop.getFreePrinter();
System.out.printf("%s ha obtingut l'impresora: %s\n", this.name, printer.getModel());
for (Document d : documents) {
printer.printDocument(d);
}
System.out.printf("%s ha alliberat l'impresora: %s\n", this.name, printer.getModel());
shop.releasePrinter(printer);
shop.getSemaphore().release();
```
El mecanisme per gestionar les impresores lliures o ocupades
es realitza mitjançant un __semàfor__ en la classe `CopyShop`,
que s'ha creat amb el número de impresores que disposa:
```java
public CopyShop() {
freePrinters = new ArrayList<>();
usedPrinters = new ArrayList<>();
freePrinters.add(new Printer("HP DeskJet 2755e", 100));
freePrinters.add(new Printer("Brother MFC-J1010DW", 120));
freePrinters.add(new Printer("Epson Workforce Pro WF-3820", 50));
// Semàfor que permet tants clients com impresores en la tenda
semaphore = new Semaphore(freePrinters.size());
}
```
A més, podem observar que els mètodes `getFreePrinter()` i `releasePrinter()`
s'han definit com `synchronized`. Això s'ha fet perquè els dos mètodes
modifiquen les llistes `freePrinters` i `usedPrinters`, i volem evitar que
diversos fils puguen modificar les llistes a la vegada i ens evitem
problemes derivats de la concurrència.
```java
public synchronized Printer getFreePrinter(){
Printer p = freePrinters.remove(0);
usedPrinters.add(p);
return p;
}
public synchronized void releasePrinter(Printer p){
freePrinters.add(p);
usedPrinters.remove(p);
}
```
#### Codi font
- <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/CopyShop.java" download="CopyShop.java">CopyShop.java</a>
```java
package ud2.examples.copyshop;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
public class CopyShop {
private List<Printer> freePrinters;
private List<Printer> usedPrinters;
private Semaphore semaphore;
public CopyShop() {
freePrinters = new ArrayList<>();
usedPrinters = new ArrayList<>();
freePrinters.add(new Printer("HP DeskJet 2755e", 100));
freePrinters.add(new Printer("Brother MFC-J1010DW", 120));
freePrinters.add(new Printer("Epson Workforce Pro WF-3820", 50));
// Semàfor que permet tants clients com impresores en la tenda
semaphore = new Semaphore(freePrinters.size());
}
public Semaphore getSemaphore() {
return semaphore;
}
public synchronized Printer getFreePrinter(){
Printer p = freePrinters.remove(0);
usedPrinters.add(p);
return p;
}
public synchronized void releasePrinter(Printer p){
freePrinters.add(p);
usedPrinters.remove(p);
}
public static void main(String[] args) {
CopyShop shop = new CopyShop();
List<CustomerThread> customers = new ArrayList<>();
CustomerThread bob = new CustomerThread("Bob", shop);
bob.addDocument("PSP Work", 10);
bob.addDocument("TFG", 70);
customers.add(bob);
CustomerThread alice = new CustomerThread("Alice", shop);
alice.addDocument("Chopin - Nocturnes, Op. 9: No. 2 in E-Flat Major", 5);
alice.addDocument("Chopin - Nocturne in C-sharp minor, Op. posth.", 4);
alice.addDocument("Beethoven - Für Elise", 3);
alice.addDocument("The Real Book - Volume I", 247);
customers.add(alice);
CustomerThread patrick = new CustomerThread("Patrick", shop);
patrick.addDocument("Don Quijote de la Mancha", 145);
customers.add(patrick);
CustomerThread pere = new CustomerThread("Pere", shop);
pere.addDocument("Tirant lo Blanc", 276);
customers.add(pere);
CustomerThread mar = new CustomerThread("Mar", shop);
mar.addDocument("Joshua Bloch - Effective Java", 50);
customers.add(mar);
for(CustomerThread customer : customers){
customer.start();
}
for(CustomerThread customer : customers){
try {
customer.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
```
- <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/Document.java" download="Document.java">Document.java</a>
```java
package ud2.examples.copyshop;
public class Document {
private String title;
private int pages;
public Document(String name, int pages) {
this.title = name;
this.pages = pages;
}
public String getTitle() {
return title;
}
public int getPages() {
return pages;
}
}
```
- <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/Printer.java" download="Printer.java">Printer.java</a>
```java
package ud2.examples.copyshop;
public class Printer {
private String model;
private int delay;
public Printer(String model, int delay) {
this.model = model;
this.delay = delay;
}
public String getModel() {
return model;
}
public void printDocument(Document d){
try {
int milis = d.getPages()*delay;
Thread.sleep(milis);
System.out.printf("Printer %s: Printed document %s with %d pages (%.2f seconds).\n",
model,
d.getTitle(),
d.getPages(),
milis/1000.0
);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
```
- <a href="/itb/DAM-PSP/files/ud2/examples/copyshop/CustomerThread.java" download="CustomerThread.java">CustomerThread.java</a>
```java
package ud2.examples.copyshop;
import java.util.ArrayList;
import java.util.List;
public class CustomerThread extends Thread {
private List<Document> documents;
private String name;
private CopyShop shop;
public CustomerThread(String name, CopyShop shop) {
this.name = name;
this.documents = new ArrayList<>();
this.shop = shop;
}
public List<Document> getDocuments() {
return documents;
}
public void addDocument(String title, int pages) {
documents.add(new Document(title, pages));
}
@Override
public void run() {
try {
shop.getSemaphore().acquire();
Printer printer = shop.getFreePrinter();
System.out.printf("%s ha obtingut l'impresora: %s\n", this.name, printer.getModel());
for (Document d : documents) {
printer.printDocument(d);
}
System.out.printf("%s ha alliberat l'impresora: %s\n", this.name, printer.getModel());
shop.releasePrinter(printer);
shop.getSemaphore().release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
```
### PitStop
#### Explicació
Aquest exemple simula una parada en bòxers d'un cotxe de Formula 1.
Quan un cotxe para, hi ha una persona encarregada de elevar el cotxe per poder canviar
les rodes del cotxe. Cada roda del cotxe és canviada per una persona diferent.
Una vegada d'han canviat totes les rodes, es pot tornar a baixar el cotxe i continuar la carrera.
En el següent codi, creem un mecànic que s'encarrega de pujar i baixar el cotxe, i altres
quatre mecànics que s'encarregen de canviar les rodes.
L'ordre de les acciona hauria de ser:
- El mecànic alça el cotxe.
- Els mècanics de les rodes esperen a que el cotxe estiga alçat.
- Els mècanics de les rodes canvien les rodes.
- El mecànic espera a que totes les rodes hagen segut canviades.
- El mecànic baixa el cotxe.
Per realitzar aquestes esperes, s'han definit els següents mètodes en la classe `Car``:
```java
public synchronized void raise() throws InterruptedException {
Thread.sleep(500);
raised = true;
notifyAll();
}
public synchronized void release() throws InterruptedException {
while(!readyToRelease()) wait();
Thread.sleep(500);
raised = false;
notifyAll();
}
public boolean readyToRelease(){
for(Tire t : tires){
if(t.getRemainingKilometers() != 100)
return false;
}
return true;
}
public synchronized void replaceTire(Tire t) throws InterruptedException{
while(!raised) wait();
Thread.sleep(ThreadLocalRandom.current().nextInt(500, 1000));
t.replace();
notify();
}
```
Si un fil intenta canviar una roda (`replaceTire`), haurà d'esperar fins que el cotxe estiga en alt (`raised`).
Això es possible gràcies al mètode `wait()`.
Si un altre fil alça el cotxe amb el mètode `raise()`, notificarà a tots els possibles fils que estiguen esperant
que continuen la seua execució. Com el valor de la variable `raised` ha canviat, els fils poden continuar la seua execució.
El mateix passa quan un fil intenta baixar el cotxe amb `release()`. Fins que no s'hagen canviat totes les rodes
no es podrà baixar el cotxe.
#### Codi font
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/PitStop.java" download="PitStop.java">PitStop.java</a>
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Car.java" download="Car.java">Car.java</a>
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Tire.java" download="Tire.java">Tire.java</a>
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/Mechanic.java" download="Mechanic.java">Mechanic.java</a>
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/TireMechanic.java" download="TireMechanic.java">TireMechanic.java</a>
- <a href="/itb/DAM-PSP/files/ud2/examples/pitstop/RaiseMechanic.java" download="RaiseMechanic.java">RaiseMechanic.java</a>