Salta el contingut
 

Pràctica 2: Programació de fils

Autor: Joan Puigcerver Ibáñez

Correu electrònic: j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Objectius

Desenvolupar aplicacions en Java que gestionen i creen diferents fils d'execució.

Entrega

L'entrega ha de complir els següents requisits:

  • Package: ud2.practices
  • El nom de la classe on comença l'execució (main()) de cada exercici és el títol de l'exercici.
  • El format de la eixida del programa ha de ser consistent amb el format demanat.
  • S'ha d'entregar un fitxer .zip amb el contingut del package ud2.practices.
  • El codi ha d'estar pujat a GitHub en el vostre repositori de l'assignatura.
  • Tag GitHub: PracticeThreads (StackOverflow: Create a tag in a GitHub repository)

CollectiveTasks

  • Package: ud2.practices.collectivetasks

Els treballadors de l'empresa informàtica s'han adonat que la faena dels Managers sols és assignar tasques als treballadors i comprovar que s'han realitat, i han decidit prescindir d'ells.

Per gestionar les tasques, cada equip Team ha creat una borsa de tasques que s'han de realitzar. Cada empleat de l'equip agafarà una tasca de la borsa i la realitzarà. Les tasques han de passar per dos estats abans de assignar-les com finalitzades.

Crea el programa CollectiveTasks que simule la realització d'un projecte on diferents equips realitzen les seues tasques sense necessitat de cap manager.

S'han d'implementar els següents mètodes:

  • Task::work(): Aquest mètode defineix com es completa una tasca. Aquest mètode ha d'obtenir el fil actual i fer que espere tants mil·lisegons com s'ha definit en la tasca.

    Aquest mètode imprimirà un missatge quan comence la tasca i quan es finalitze:

    Pere: Starting task Main page design...
    Pere: Finished task Main page design (2500 ms).
    
  • Task::test(): Aquest mètode defineix com es prova una tasca. Aquest mètode ha d'obtenir el fil actual i fer que espere la meitat de mil·lisegons com s'ha definit en la tasca.

    Aquest mètode imprimirà un missatge quan comence la tasca i quan es finalitze:

    Pere: Testing task Main page design...
    Pere: Finished testing task Main page design (1250 ms).
    
  • EmployeeThread::run(): Aquest mètode defineix el comportament de cada treballador. Aquest mètode obtindrà la següent tasca a realitzar del seu equip Team.

    • Si la Task no està completada:
      • Realitzar la tasca amb Task::work()
      • Una vegada completada, afegir la tasca a Task::testingTasks
    • Si la Task no està provada:
      • Provar la tasca amb Task::test()
      • Una vegada completada, afegir la tasca a Task::finishedTasks
    • Si la Task és null:
      • Ja s'han completat totes les tasques.

    S'ha de mostrar un missatge amb la tasca obtinguda:

    Pere: Assigned to task Main page design.
    Pere: No more pending tasks. Going home.
    

  • Team::getNextTask(): Aquest mètode retorna la següent tasca que l'equip ha de realitzar. Retornarà la primera tasca de unfinishedTasks. Si no queden tasques en aquesta llista, retornarà la primera tasca de testingTasks. Si no queden tasques en cap llista, retornarà null.

  • CollectiveTasks::main(): Aquest mètode crea els equips i les tasques, cal acabar-lo perquè cada equip comence a treballar en les tasques assignades. Mostrarà el següent missatge quan tots els equips acaben les seues tasques:

    Project finished! Every team has finished the assigned tasks.
    

Important

S'han d'utilitzar monitors per controlar els accessos a recursos compartits. Cal modificar les definicions dels mètodes sensibles a problemes per concurrència, utilitzant syncronized.

Codi font

CollectiveTasks.java
package ud2.practices.collectivetasks;

public class CollectiveTasks {
    public static void main(String[] args) {
        Team frontend = new Team("Frontend");
        frontend.addTask("Main page design", 2500);
        frontend.addTask("Shopping cart navigation bar", 5500);
        frontend.addTask("Breadcrumb", 1500);
        frontend.addTask("Profile page", 7500);
        frontend.addEmployee(new EmployeeThread("Pere"));
        frontend.addEmployee(new EmployeeThread("Maria"));

        Team backend = new Team("Backend");
        backend.addTask("Connect API to database", 3000);
        backend.addTask("Shopping API models", 4200);
        backend.addTask("API authentication", 2100);
        backend.addTask("Testing API", 5500);
        backend.addEmployee(new EmployeeThread("Anna"));
        backend.addEmployee(new EmployeeThread("Arnau"));

        Team database = new Team("Database");
        database.addTask("Relation Model", 5000);
        database.addTask("Define models in the database", 4200);
        database.addTask("Install DBMS", 3000);
        database.addTask("Views", 2800);
        database.addEmployee(new EmployeeThread("Mireia"));
        database.addEmployee(new EmployeeThread("Mar"));

        // TODO: Start all the teams and wait for them to finish their tasks

        System.out.println("Projecte acabat! Tots els equips han acabat les tasques assignades.");
    }
}
Team.java
package ud2.practices.collectivetasks;

import java.util.ArrayList;
import java.util.List;

public class Team {
    private final String name;
    private final List<EmployeeThread> employees;
    private final List<Task> unfinishedTasks;
    private final List<Task> testingTasks;
    private final List<Task> finishedTasks;

    public Team(String name) {
        this.name = name;
        employees = new ArrayList<>();
        unfinishedTasks = new ArrayList<>();
        testingTasks = new ArrayList<>();
        finishedTasks = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public List<EmployeeThread> getEmployees() {
        return employees;
    }

    public void addEmployee(EmployeeThread employee) {
        this.employees.add(employee);
        employee.setTeam(this);
    }

    public void addTask(String taskName, int taskDuration) {
        unfinishedTasks.add(new Task(taskName, taskDuration));
    }

    public Task getNextTask(){
        /**
         * TODO:
         * Get next Task from unfinishedTasks. If all unfinishedTaks are done,
         * get next Task from testingTasks.
         * The task must be deleted from the list when retrieved.
         * If all Tasks are done and tested, return null.
         */

        return null;
    }
}
EmployeeThread.java
package ud2.practices.collectivetasks;

import java.util.ArrayList;
import java.util.List;

public class EmployeeThread extends Thread{

    Team team;
    public EmployeeThread(String name) {
        super();
        this.setName(name);
        this.team = null;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    @Override
    public void run() {
        /*
         * TODO: Get next task from team.
         *
         * If the task needs to be done:
         *  - Do the task with work()
         *  - Add the task to testingTasks
         * If the task needs to be tested:
         *  - Test the task with test()
         *  - Add the task to finishedTasks
         * If the task is null:
         *  - All tasks are done. Exit.
         */

        System.out.printf("%s: Ha realitzat totes les tasques assignades.\n", this.getName());
    }
}
Task.java
package ud2.practices.collectivetasks;

public class Task {
    private final int duration;
    private final String name;
    private TaskStatus status;

    public Task(String name, int duration) {
        this.name = name;
        this.duration = duration;
        status = TaskStatus.UNFINISHED;
    }

    public int getDuration() {
        return duration;
    }

    public String getName() {
        return name;
    }

    public TaskStatus status() {
        return status;
    }

    public void setStatus(TaskStatus status) {
        this.status = status;
    }

    public void work(){
        Thread current = Thread.currentThread();
        System.out.printf("%s: Starting task %s...\n", current.getName(), this.name);

        // TODO: Do the task (sleep DURATION miliseconds)

        setStatus(TaskStatus.TESTING);
        System.out.printf("%s: Finished task %s (%d).\n", current.getName(), this.name, this.duration);
    }

    public void test(){
        Thread current = Thread.currentThread();
        System.out.printf("%s: Starting task %s...\n", current.getName(), this.name);

        // TODO: Do the task (sleep DURATION / 2 miliseconds)

        setStatus(TaskStatus.FINISHED);
        System.out.printf("%s: Finished task %s (%d).\n", current.getName(), this.name, this.duration);
    }
}
TaskStatus.java
package ud2.practices.collectivetasks;

public enum TaskStatus {
    UNFINISHED,
    TESTING,
    FINISHED
}

Solució

Solució CollectiveTasks
CollectiveTasks.java
package ud2.practices.collectivetasks.solution;

public class CollectiveTasks {
    public static void main(String[] args) {
        Team frontend = new Team("Frontend");
        frontend.addTask("Main page design", 2500);
        frontend.addTask("Shopping cart navigation bar", 5500);
        frontend.addTask("Breadcrumb", 1500);
        frontend.addTask("Profile page", 7500);
        frontend.addEmployee(new EmployeeThread("Pere"));
        frontend.addEmployee(new EmployeeThread("Maria"));

        Team backend = new Team("Backend");
        backend.addTask("Connect API to database", 3000);
        backend.addTask("Shopping API models", 4200);
        backend.addTask("API authentication", 2100);
        backend.addTask("Testing API", 5500);
        backend.addEmployee(new EmployeeThread("Anna"));
        backend.addEmployee(new EmployeeThread("Arnau"));

        Team database = new Team("Database");
        database.addTask("Relation Model", 5000);
        database.addTask("Define models in the database", 4200);
        database.addTask("Install DBMS", 3000);
        database.addTask("Views", 2800);
        database.addEmployee(new EmployeeThread("Mireia"));
        database.addEmployee(new EmployeeThread("Mar"));

        // TODO: Start all the teams and wait for them to finish their tasks
        frontend.getEmployees().forEach(Thread::start);
        backend.getEmployees().forEach(Thread::start);
        database.getEmployees().forEach(Thread::start);

        try {
            for (EmployeeThread e : frontend.getEmployees()) {
                e.join();
            }
            for (EmployeeThread e : backend.getEmployees()) {
                e.join();
            }
            for (EmployeeThread e : database.getEmployees()) {
                e.join();
            }
        } catch (InterruptedException e) {
            System.err.println("Error: " + e.getMessage());
        }

        System.out.println("Projecte acabat! Tots els equips han acabat les tasques assignades.");
    }
}
Team.java
package ud2.practices.collectivetasks.solution;

import java.util.ArrayList;
import java.util.List;

public class Team {
    private String name;
    private final List<EmployeeThread> employees;
    private final List<Task> unfinishedTasks;
    private final List<Task> testingTasks;
    private final List<Task> finishedTasks;

    public Team(String name) {
        this.name = name;
        employees = new ArrayList<>();
        unfinishedTasks = new ArrayList<>();
        testingTasks = new ArrayList<>();
        finishedTasks = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public List<EmployeeThread> getEmployees() {
        return employees;
    }

    public void addEmployee(EmployeeThread employee) {
        this.employees.add(employee);
        employee.setTeam(this);
    }

    public void addTask(String taskName, int taskDuration) {
        synchronized (unfinishedTasks) {
            unfinishedTasks.add(new Task(taskName, taskDuration));
        }
    }

    public Task getNextUnfinishedTask() {
        synchronized (unfinishedTasks) {
            if (unfinishedTasks.isEmpty()) {
                return null;
            }
            return unfinishedTasks.removeFirst();
        }
    }

    public void addTestingTask(Task t){
        synchronized (testingTasks) {
            testingTasks.add(t);
        }
    }

    public Task getNextTestingTask(){
        synchronized (testingTasks) {
            if (testingTasks.isEmpty()) {
                return null;
            }
            return testingTasks.removeFirst();
        }
    }

    public void addFinishedTask(Task t){
        synchronized (finishedTasks) {
            finishedTasks.add(t);
        }
    }

    public Task getNextTask(){
        /**
         * TODO:
         * Get next Task from unfinishedTasks. If all unfinishedTaks are done,
         * get next Task from testingTasks.
         * The task must be deleted from the list when retrieved.
         * If all Tasks are done and tested, return null.
         */
        Task t = getNextUnfinishedTask();
        if (t == null) t = getNextTestingTask();

        return t;
    }

}
EmployeeThread.java
package ud2.practices.collectivetasks.solution;

public class EmployeeThread extends Thread{

    Team team;
    public EmployeeThread(String name) {
        super();
        this.setName(name);
        this.team = null;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    @Override
    public void run() {
        /*
         * TODO: Get next task from team.
         *
         * If the task needs to be done:
         *  - Do the task with work()
         *  - Add the task to testingTasks
         * If the task needs to be tested:
         *  - Test the task with test()
         *  - Add the task to finishedTasks
         * If the task is null:
         *  - All tasks are done. Exit.
         */

        try {
            Task t = team.getNextTask();
            while (t != null) {
                if (t.status() == TaskStatus.UNFINISHED) {
                    t.work();
                    team.addTestingTask(t);
                } else if (t.status() == TaskStatus.TESTING) {
                    t.test();
                    team.addFinishedTask(t);
                }

                t = team.getNextTask();
            }

            System.out.printf("%s: Ha realitzat totes les tasques assignades.\n", this.getName());
        } catch (InterruptedException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}
Task.java
package ud2.practices.collectivetasks.solution;

public class Task {
    private final int duration;
    private final String name;
    private TaskStatus status;

    public Task(String name, int duration) {
        this.name = name;
        this.duration = duration;
        status = TaskStatus.UNFINISHED;
    }

    public int getDuration() {
        return duration;
    }

    public String getName() {
        return name;
    }

    public TaskStatus status() {
        return status;
    }

    public void setStatus(TaskStatus status) {
        this.status = status;
    }

    public void work() throws InterruptedException {
        Thread current = Thread.currentThread();
        System.out.printf("%s: Starting task %s...\n", current.getName(), this.name);

        // TODO: Do the task (sleep DURATION miliseconds)
        Thread.sleep(this.duration);

        setStatus(TaskStatus.TESTING);
        System.out.printf("%s: Finished task %s (%d).\n", current.getName(), this.name, this.duration);
    }

    public void test() throws InterruptedException {
        Thread current = Thread.currentThread();
        System.out.printf("%s: Starting task %s...\n", current.getName(), this.name);

        // TODO: Do the task (sleep DURATION / 2 miliseconds)
        Thread.sleep(this.duration / 2);

        setStatus(TaskStatus.FINISHED);
        System.out.printf("%s: Finished task %s (%d).\n", current.getName(), this.name, this.duration / 2);
    }
}
TaskStatus.java
package ud2.practices.collectivetasks.solution;

public enum TaskStatus {
    UNFINISHED,
    TESTING,
    FINISHED
}

SwimmingPool

  • Package: ud2.practices.swimmingpool

Crea el programa SwimmingPool que simule una piscina pública, on diferents persones poden accedir als recursos de la piscina simultàniament.

Els recursos que es disposen són:

  • Una piscina amb capacitat per 10 persones.
  • Dutxes amb capacitat per 3 persones.
  • Una zona per descansar.

Cada persona s'executarà en un fil diferent. Totes les persones realitzaran les següents accions:

  • Descansar entre 1 i 5 segons aleatòriament.
  • Dutxar-se (2 segons).
  • Banyar-se entre 1 i 10 segons aleatòriament.

Les accions de cada persona es repetiran indefinidament.

El programa principal esperarà 1 minut i interromprà l'execució de totes les persones.

S'han de mostrar els següents missatges quan una persona canvie d'estat respecte a les accions anteriorment esmentades:

  • La persona està descansant:

    Pere està descansant 4.2 segons.
    

  • La persona va a la dutxa:

    Pere vol dutxarse.
    

  • La persona està dutxant-se:

    Pere està dutxant-se.
    

  • La persona ha acabat de dutxar-se:

    Pere ha acabat de dutxar-se.
    

  • La persona està esperant per entrar a la piscina:

    Pere vol nadar.
    

  • La persona està nadant.

    Pere està nadant 2.3 segons.
    

  • La persona ha eixit de la piscina.

    Pere ha eixit de la piscina.
    

  • Han tirat a la persona de la piscina.

    Pere ha abandonat les instal·lacions.
    

Codi font

SwimmingPool.java
package ud2.practices.swimmingpool;

import java.util.ArrayList;
import java.util.List;

public class SwimmingPool {
    // TODO: Add semaphores

    public SwimmingPool(int poolCapacity, int showersCapacity) {
        // TODO: Create semaphores
    }

    // TODO: create get() method for semaphores

    public static void main(String[] args) {
        SwimmingPool pool = new SwimmingPool(10, 3);
        String[] names = {
            "Andrès", "Àngel", "Anna", "Carles", "Enric",
            "Helena", "Isabel", "Joan", "Lorena", "Mar",
            "Maria", "Marta", "Míriam", "Nicolàs", "Òscar",
            "Paula", "Pere", "Teresa", "Toni", "Vicent"
        };
        List<PersonThread> persons = new ArrayList<>();
        for(String name : names) {
            // TODO: Create the threads and start them
        }

        // TODO: Wait 60 seconds and kick all persons

        // TODO: Wait for all persons to leave

        System.out.println("Tothom ha marxat de la piscina.");
    }
}
PersonThread.java
package ud2.practices.swimmingpool;

public class PersonThread extends Thread {
    private final SwimmingPool pool;

    public PersonThread(String name, SwimmingPool pool) {
        super(name);
        this.pool = pool;
    }

    /**
     * TODO: The persons rests between 1 and 5 seconds
     * @throws InterruptedException: if the thread is interrupted
     */
    public void rest() throws InterruptedException {
        int milis = 0;
        System.out.printf("%s està descansant %.2f segons.\n", getName(), milis / 1000.0);
    }

    /**
     * TODO: The persons takes a shower for 2 seconds
     * - Tries to get into a shower
     * - Takes a shower
     * - Leaves the showers
     * @throws InterruptedException: if the thread is interrupted
     */
    public void takeShower() throws InterruptedException {
        int milis = 2000;
        System.out.printf("%s vol dutxar-se.\n", getName());
        System.out.printf("%s està dutxant-se.\n", getName());
        System.out.printf("%s ha acabat de dutxar-se.\n", getName());
    }

    /**
     * TODO: The persons swims between 1 and 10 seconds
     * - Tries to get into the swimming pool
     * - Swims
     * - Leaves the swimming pool
     * @throws InterruptedException: if the thread is interrupted
     */
    public void swim() throws InterruptedException {
        int milis = 0;
        System.out.printf("%s vol nadar.\n", getName());
        System.out.printf("%s està nadant %.2f segons.\n", getName(), milis / 1000.0);
        System.out.printf("%s ha eixit de la piscina.\n", getName());
    }

    @Override
    public void run() {
        while(true) {
            // TODO: Rests
            // TODO: Takes a shower
            // TODO: Swims
        }
        System.out.printf("%s ha abandonat les instal·lacions.\n", getName());
    }
}

Solució

Solució SwimmingPool
SwimmingPool.java
package ud2.practices.swimmingpool.solution;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;

public class SwimmingPool {
    // TODO: Add semaphores
    private final Semaphore poolSemaphore;
    private final Semaphore showersSemaphore;

    public SwimmingPool(int poolCapacity, int showersCapacity) {

        // TODO: Create semaphores
        poolSemaphore = new Semaphore(poolCapacity);
        showersSemaphore = new Semaphore(showersCapacity);
    }

    // TODO: create get() method for semaphores
    public Semaphore getPoolSemaphore() {
        return poolSemaphore;
    }

    public Semaphore getShowersSemaphore() {
        return showersSemaphore;
    }

    public static void main(String[] args) {
        SwimmingPool pool = new SwimmingPool(10, 3);
        String[] names = {
            "Andrès", "Àngel", "Anna", "Carles", "Enric",
            "Helena", "Isabel", "Joan", "Lorena", "Mar",
            "Maria", "Marta", "Míriam", "Nicolàs", "Òscar",
            "Paula", "Pere", "Teresa", "Toni", "Vicent"
        };
        List<PersonThread> persons = new ArrayList<>();
        for(String name : names) {
            PersonThread p = new PersonThread(name, pool);
            persons.add(p);
            p.start();
        }

        try {
            // TODO: Wait 60 seconds and kick all persons
            Thread.sleep(60000);

            // TODO: Wait for all persons to leave
            for (PersonThread p : persons) {
                p.join();
            }
        } catch (InterruptedException e) {
            System.err.println("Error: " + e.getMessage());
        }

        System.out.println("Tothom ha marxat de la piscina.");
    }
}
PersonThread.java
package ud2.practices.swimmingpool.solution;

import java.util.concurrent.ThreadLocalRandom;

public class PersonThread extends Thread {
    private final SwimmingPool pool;

    public PersonThread(String name, SwimmingPool pool) {
        super(name);
        this.pool = pool;
    }

    /**
     * TODO: The persons rests between 1 and 5 seconds
     * @throws InterruptedException: if the thread is interrupted
     */
    public void rest() throws InterruptedException {
        int milis = ThreadLocalRandom.current().nextInt(1000, 5000);
        System.out.printf("%s està descansant %.2f segons.\n", getName(), milis / 1000.0);

        Thread.sleep(milis);
    }

    /**
     * TODO: The persons takes a shower for 2 seconds
     * - Tries to get into a shower
     * - Takes a shower
     * - Leaves the showers
     * @throws InterruptedException: if the thread is interrupted
     */
    public void takeShower() throws InterruptedException {
        int milis = 2000;
        System.out.printf("%s vol dutxar-se.\n", getName());
        pool.getShowersSemaphore().acquire();
        System.out.printf("%s està dutxant-se.\n", getName());
        Thread.sleep(milis);
        pool.getShowersSemaphore().release();
        System.out.printf("%s ha acabat de dutxar-se.\n", getName());
    }

    /**
     * TODO: The persons swims between 1 and 10 seconds
     * - Tries to get into the swimming pool
     * - Swims
     * - Leaves the swimming pool
     * @throws InterruptedException: if the thread is interrupted
     */
    public void swim() throws InterruptedException {
        int milis = ThreadLocalRandom.current().nextInt(1000, 10000);
        System.out.printf("%s vol nadar.\n", getName());
        pool.getPoolSemaphore().acquire();
        System.out.printf("%s està nadant %.2f segons.\n", getName(), milis / 1000.0);
        Thread.sleep(milis);
        System.out.printf("%s ha eixit de la piscina.\n", getName());
        pool.getPoolSemaphore().release();
    }

    @Override
    public void run() {
        try {
            while(!isInterrupted()) {
                // TODO: Rests
                rest();
                // TODO: Takes a shower
                takeShower();
                // TODO: Swims
                swim();
            }
        } catch (InterruptedException e) {
            System.out.printf("%s interromput.\n", getName());
        }
        System.out.printf("%s ha abandonat les instal·lacions.\n", getName());
    }
}

LyricsPlayer

  • Package: ud2.practices.lyrics

Crea el programa LyricsPlayer que és un reproductor de lletres de cançons, que simula com treballen els reproductors de música.

El programa crearà dos fils diferents:

  • Player: Que reproduirà la cançó escollida. El reproductor mostrara una paraula de la lletra cada mig segon (0.5s).
  • Loader: Que s'encarrega de descarregar la cançó en segon pla.

    Aquest fil llegirà les lletres des del fitxer de text: files/ud2/lyrics.txt

Aquest programa seguirà l'estructura bàsica de Producer/Consumer:

  • El fil Loader obté les línies del fitxer i les guarda en la llista de línies.
  • El fil Player obté les línies a mostrar a partir de la llista LyricsPlayer::lines i les mostra per pantalla.

Crea els mecanismes de sincronització necessaris perquè:

  • El fil Player espere a què hi haja alguna línia disponible per poder reproduir-la.
  • El fil Loader notifique que ha llegit una línia.
  • No hi haja accessos simultanis a la llista LyricsPlayer::lines.

Codi font

LyricsPlayer.java
package ud2.practices.lyrics;

import java.util.ArrayList;
import java.util.List;

public class LyricsPlayer {
    private final Player player;
    private final Loader loader;
    private final List<String> lines;
    private boolean end;

    public LyricsPlayer(String filename) {
        player = new Player(this);
        loader = new Loader(this, filename);
        lines = new ArrayList<>();
        end = false;
    }

    public void setEnd(boolean end) {
        this.end = end;
    }
    public boolean ended(int i) {
        return end && i >= lines.size();
    }

    public boolean isLineAvailable(int i){
        return i < lines.size();
    }

    public String getLine(int i){
        return lines.get(i);
    }

    public int addLine(String line){
        lines.add(line);
        return lines.size() - 1;
    }

    public void start(){
        player.start();
        loader.start();
    }
    public void join() throws InterruptedException {
        player.join();
        loader.join();
    }
    public void interrupt() {
        player.interrupt();
        loader.interrupt();
    }

    public static void main(String[] args) {
        LyricsPlayer lyricsPlayer = new LyricsPlayer("files/ud2/lyrics.txt");
        lyricsPlayer.start();

        try {
            lyricsPlayer.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Player.java
package ud2.practices.lyrics;

public class Player extends Thread {
    private final LyricsPlayer lyricsPlayer;
    private int lyricsIndex;

    public Player(LyricsPlayer lyricsPlayer) {
        this.lyricsPlayer = lyricsPlayer;
        lyricsIndex = 0;
    }

    public void playLine(int i) throws InterruptedException {
        String[] line = lyricsPlayer.getLine(i).split(" ");

        for (int j = 0; j < line.length; j++) {
            Thread.sleep(500);
            if(j == 0)
                System.out.printf("%d: ", i);
            else
                System.out.print(" ");
            System.out.print(line[j]);
        }
        System.out.println();
    }

    @Override
    public void run() {
        try {
            while(!lyricsPlayer.ended(lyricsIndex)){
                this.playLine(lyricsIndex);
                lyricsIndex++;
            }
        } catch (Exception e) {
            System.err.println(e.getMessage());
            lyricsPlayer.interrupt();
        }
    }
}
Loader.java
package ud2.practices.lyrics;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.concurrent.ThreadLocalRandom;

public class Loader extends Thread {
    private final LyricsPlayer lyricsPlayer;
    private final String filename;

    public Loader(LyricsPlayer lyricsPlayer, String filename) {
        this.lyricsPlayer = lyricsPlayer;
        this.filename = filename;
    }

    @Override
    public void run() {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = br.readLine()) != null) {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 10000));
                int i = this.lyricsPlayer.addLine(line);
                System.err.printf(" (Line %d loaded) ", i);
            }
            this.lyricsPlayer.setEnd(true);
        } catch (Exception e) {
            System.err.println(e.getMessage());
            lyricsPlayer.interrupt();
        }
    }
}

Solució

Solució LyricsPlayer
LyricsPlayer.java
package ud2.practices.lyrics.solution;

import java.util.ArrayList;
import java.util.List;

public class LyricsPlayer {
    private final Player player;
    private final Loader loader;
    private final List<String> lines;
    private boolean end;

    public LyricsPlayer(String filename) {
        player = new Player(this);
        loader = new Loader(this, filename);
        lines = new ArrayList<>();
        end = false;
    }

    public synchronized void setEnd(boolean end) {
        this.end = end;
    }
    public synchronized boolean ended(int i) {
        return end && i >= lines.size();
    }

    public synchronized boolean isLineAvailable(int i){
        return i < lines.size();
    }

    public synchronized String getLine(int i) throws InterruptedException {
        while (!isLineAvailable(i)) wait();

        return lines.get(i);
    }

    public synchronized int addLine(String line){
        lines.add(line);
        notifyAll();
        return lines.size() - 1;
    }


    public void start(){
        player.start();
        loader.start();
    }
    public void join() throws InterruptedException {
        player.join();
        loader.join();
    }
    public void interrupt() {
        player.interrupt();
        loader.interrupt();
    }

    public static void main(String[] args) {
        LyricsPlayer lyricsPlayer = new LyricsPlayer("files/ud2/lyrics.txt");
        lyricsPlayer.start();

        try {
            lyricsPlayer.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Player.java
package ud2.practices.lyrics.solution;

public class Player extends Thread {
    private final LyricsPlayer lyricsPlayer;
    private int lyricsIndex;

    public Player(LyricsPlayer lyricsPlayer) {
        this.lyricsPlayer = lyricsPlayer;
        lyricsIndex = 0;
    }

    public void playLine(int i) throws InterruptedException {
        String[] line = lyricsPlayer.getLine(i).split(" ");

        for (int j = 0; j < line.length; j++) {
            Thread.sleep(500);
            if(j == 0)
                System.out.printf("%d: ", i);
            else
                System.out.print(" ");
            System.out.print(line[j]);
        }
        System.out.println();
    }

    @Override
    public void run() {
        try {
            while(!lyricsPlayer.ended(lyricsIndex)){
                this.playLine(lyricsIndex);
                lyricsIndex++;
            }
        } catch (Exception e) {
            System.err.println(e.getMessage());
            lyricsPlayer.interrupt();
        }
    }
}
Loader.java
package ud2.practices.lyrics.solution;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.concurrent.ThreadLocalRandom;

public class Loader extends Thread {
    private LyricsPlayer lyricsPlayer;
    private String filename;

    public Loader(LyricsPlayer lyricsPlayer, String filename) {
        this.lyricsPlayer = lyricsPlayer;
        this.filename = filename;
    }

    @Override
    public void run() {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = br.readLine()) != null) {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 10000));
                int i = this.lyricsPlayer.addLine(line);
                System.err.printf(" (Line %d loaded) ", i);
            }
            this.lyricsPlayer.setEnd(true);
        } catch (Exception e) {
            System.err.println(e.getMessage());
            lyricsPlayer.interrupt();
        }
    }
}