Initial commit

This commit is contained in:
github-classroom[bot]
2022-03-31 09:49:56 +00:00
commit 5a1de0b8e7
83 changed files with 17003 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
/*
* Gradle build configuration for specific lab module / exercise
*/
// enabled plugins
plugins {
id 'java'
// Apply the application plugin to add support for building a CLI application.
id 'application'
}
// Project/Module information
description = 'Lab04 Philosopher'
group = 'ch.zhaw.prog2'
version = '2022.1'
// Dependency configuration
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.+'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.+'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.+'
testImplementation 'org.mockito:mockito-core:4.3.1'
}
// Test task configuration
test {
// Use JUnit platform for unit tests
useJUnitPlatform()
testLogging {
events "PASSED", "SKIPPED", "FAILED"
}
}
// Configuration for Application plugin
application {
// Define the main class for the application.
mainClass = 'ch.zhaw.prog2.philosopher.PhilosopherGui'
}
// Java plugin configuration
java {
// By default the Java version of the gradle process is used as source/target version.
// This can be overridden, to ensure a specific version. Enable only if required.
// sourceCompatibility = JavaVersion.VERSION_17 // ensure Java source code compatibility
// targetCompatibility = JavaVersion.VERSION_17 // version of the created byte-code
// Java compiler specific options
compileJava {
// source files should be UTF-8 encoded
options.encoding = 'UTF-8'
// for more options see https://docs.gradle.org/current/dsl/org.gradle.api.tasks.compile.CompileOptions.html
}
}
@@ -0,0 +1,221 @@
package ch.zhaw.prog2.philosopher;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static ch.zhaw.prog2.philosopher.ForkManager.ForkState.*;
/**
* ForkManager manages the resources (=forks), used by the philosophers
*/
class ForkManager {
// maximum concurrent threads acquiring forks - used for internal statistics
private static final LockFreeMax concurrentAcquires = new LockFreeMax();
private final Lock mutex; // shared mutex for all forks
private final int delayTime; // delay in milliseconds between acquiring / releasing the forks
private final int numForks; // amount of forks to be managed
private final Fork[] forks; // array of managed forks
/**
* Constructor to initialize the fork manager.
* @param numForks amount of forks to manage
* @param delayTime delay in milliseconds between acquiring / releasing the forks
*/
public ForkManager(int numForks, int delayTime) {
this.mutex = new ReentrantLock();
this.delayTime = delayTime;
this.numForks = numForks;
this.forks = new Fork[numForks];
for (int forkId = 0; forkId < numForks; forkId++)
forks[forkId] = new Fork(forkId, mutex);
}
/**
* Acquire both forks of a specific philosopher.
* Here you implement your synchronization strategy.
* There should be always a delay between taking the two forks (see {@link #forkDelay()})
* @param philosopherId id of the philosopher
*/
public void acquireForks(int philosopherId) throws InterruptedException {
// acquire forks sequentially
leftFork(philosopherId).acquire(philosopherId);
forkDelay();
rightFork(philosopherId).acquire(philosopherId);
}
/**
* Release both forks for a specific philosopher.
* There should be always a delay between releasing the two forks.
*
* @param philosopherId id of the philosopher
*/
public void releaseForks(int philosopherId) throws InterruptedException {
// order of releasing does not matter
leftFork(philosopherId).release(philosopherId);
forkDelay();
rightFork(philosopherId).release(philosopherId);
}
/**
* Returns the right fork of the given philosopher
* @param philosopherId id of philosopher to get the right fork
* @return right fork of the specified philosopher
*/
private Fork rightFork(int philosopherId) {
return forks[(numForks + philosopherId - 1) % numForks];
}
/**
* Returns the left fork of the given philosopher
* @param philosopherId id of philosopher to get the left fork
* @return left fork of the specified philosopher
*/
private Fork leftFork(int philosopherId) {
return forks[philosopherId];
}
/**
* Waits {@link #delayTime} milliseconds between taking the forks.
* @throws InterruptedException if the thread is interrupted during wait
*/
void forkDelay() throws InterruptedException {
try {
Thread.sleep(this.delayTime);
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted fork delay - " + e.getMessage());
}
}
/**
* Get the maximum number of threads concurrently acquired forks - used to measure parallelism.
* @return maximum number of concurrent threads acquiring forks
*/
public int getConcurrentAcquires() {
return concurrentAcquires.maxValue.intValue();
}
/**
* Test if all forks are in a specific state. Used to detect deadlocks.
* @param state State of fork to test for
* @return true if all forks are in the given state, false otherwise
*/
public boolean areAllForksInState(ForkState state) {
return Arrays.stream(forks).allMatch(fork -> fork.state == state);
}
@Override
public String toString() {
return "forks = " + Arrays.toString(forks);
}
/**
* Possible states of a fork.
* The WAITING state is used as a temporary state to mark an OCCUPIED fork,
* if another philosopher is waiting for it (see {@link Fork#acquire(int)}.
*/
enum ForkState {
FREE, WAITING, OCCUPIED
}
/**
* Class holding the state of a single fork.
* It also provides a condition (waiting room) to allow philosophers to wait for this fork.
*/
private static class Fork {
private final int id; // unique id of fork
private final Lock mutex; // shared mutex of fork-manager
private final Condition cond; // specific wait condition for this fork
private ForkState state = FREE; // current state of the fork
private int ownerId; // temporary owning philosopher of the fork
/**
* Constructor for a fork.
* @param id unique id of the fork
* @param mutex shared mutex to use for wait conditions
*/
public Fork(int id, Lock mutex) {
this.id = id;
this.ownerId = -1;
this.mutex = mutex;
this.cond = mutex.newCondition();
}
/**
* Acquire the fork for a specific philosopher (applicantId).
* The applicantId is used to remember the current "owner" of a fork.
* @param applicantId id of the philosopher to acquire the fork
* @throws InterruptedException if the thread is interrupted during waiting for the fork
*/
public void acquire(int applicantId) throws InterruptedException {
try {
mutex.lock();
while (state != FREE) {
state = WAITING;
cond.await();
}
concurrentAcquires.increment();
TimeUnit.MILLISECONDS.sleep(10);
state = OCCUPIED;
ownerId = applicantId;
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted acquire fork " + id + " - " + e.getMessage());
} finally {
concurrentAcquires.decrement();
mutex.unlock();
}
}
/**
* Release the fork for a specific philosopher (applicantId).
* The applicantId is used to verify that a fork is released by the "owner".
* @param applicantId id of the philosopher releasing the fork
* @throws InterruptedException if the thread is interrupted during releasing the fork
*/
public void release(int applicantId) throws InterruptedException {
try {
mutex.lock();
if (ownerId != applicantId) throw new IllegalStateException("Release fork " + id + " not owned by " + applicantId);
TimeUnit.MILLISECONDS.sleep(10);
state = FREE;
ownerId = -1;
cond.signal();
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted release fork " + id + " - " + e.getMessage());
} finally {
mutex.unlock();
}
}
@Override
public String toString() {
return "Fork {" + "id=" + id + ", state=" + state + ", owner=" + ownerId + '}';
}
}
/*
* Determine maximum value without using locks
*
* This class is used here to detect the maximum numbers of threads that can be in the same codeblock.
* To get a good measurement:
* - Use a sleep() between increment() and decrement()
* - Use increment() after acquiring the lock and decrement() before releasing the lock
* - Do not call any method that does release the lock: Do not use await() between increment() and decrement()
*/
private static class LockFreeMax {
private final AtomicInteger value = new AtomicInteger(0);
private final AtomicInteger maxValue = new AtomicInteger(0);
public void increment() {
maxValue.accumulateAndGet(value.incrementAndGet(), Math::max);
}
public void decrement() {
value.decrementAndGet();
}
}
}
@@ -0,0 +1,163 @@
package ch.zhaw.prog2.philosopher;
import ch.zhaw.prog2.philosopher.PhilosopherTable.Philosopher;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Observable;
import java.util.Observer;
import static ch.zhaw.prog2.philosopher.PhilosopherTable.PhilosopherState.*;
/**
* Main- and Graphical-UI classes for the philosopher exercise.
* You should not change these classes, just use it to run the application.
*/
public class PhilosopherGui extends JFrame {
private static final int DEFAULT_PHILOSOPHER_COUNT = 5;
private static final int DEFAULT_BASE_TIME = 75; // milliseconds
private final PhilosopherTable table;
public static void main(String[] args) {
int philosopherCount = args.length >=1 ? Integer.parseInt(args[0]) : DEFAULT_PHILOSOPHER_COUNT;
int baseTime = args.length >= 2 ? Integer.parseInt(args[1]) : DEFAULT_BASE_TIME;
new PhilosopherGui(philosopherCount, baseTime);
}
public PhilosopherGui(int philosopherCount, int baseTime) {
setTitle("Philosopher");
setVisible(true);
setVisible(false);
Insets insets = getInsets();
setSize(insets.left + insets.right + 400, insets.top + insets.bottom + 400);
table = new PhilosopherTable(philosopherCount, baseTime);
PhilosopherPanel panel = new PhilosopherPanel(table, philosopherCount);
new ConsoleLogger(table);
table.start();
setContentPane(panel);
setVisible(true);
repaint();
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
closing();
}
});
}
private void closing() {
table.stop();
System.exit(0);
}
static class PhilosopherPanel extends JPanel implements Observer {
private static final long serialVersionUID = 5113281871592746242L;
private final PhilosopherTable table;
private final int philosopherCount; // greater than 2
public PhilosopherPanel(PhilosopherTable table, int philosopherCount) {
this.philosopherCount = philosopherCount;
this.table = table;
table.addObserver(this);
//new Timer(100, e -> repaint()).start(); // autorepaint periodically
}
public void paint(Graphics g) {
Insets insets = getInsets();
Dimension dim = getSize();
int length = Math.min(dim.width, dim.height);
double teta;
double tetaIn;
double phi = 2 * Math.PI / philosopherCount;
int plateRadius = (int) (Math.sqrt(Math.pow(length / 2, 2.0)
- Math.pow(Math.cos(phi) * (length / 2), 2.0) + Math.sin(phi)
* (length / 2)) * 0.25);
int tableRadius = (int) (length / 2 - plateRadius) - 10;
int halfStickLength = (int) (plateRadius * 1.25);
int centerX = length / 2 + insets.left;
int centerY = length / 2 + insets.top;
super.paint(g);
for (int philosopherId = 0; philosopherId < philosopherCount; philosopherId++) {
int transCenterX = centerX - plateRadius;
int transCenterY = centerY - plateRadius;
teta = 0;
switch (table.getPhilosopher(philosopherId).getState()) {
case THINKING:
g.setColor(Color.blue);
break;
case HUNGRY:
g.setColor(Color.red);
break;
case EATING:
g.setColor(Color.yellow);
break;
}
int xPositionPlate = (int) Math.round(transCenterX + tableRadius * Math.cos(philosopherId * phi));
int yPositionPlate = (int) Math.round(transCenterY + tableRadius * Math.sin(philosopherId * phi));
g.fillOval(xPositionPlate, yPositionPlate, 2 * plateRadius, 2 * plateRadius);
g.setColor(Color.black);
g.setFont(new Font(Font.DIALOG, Font.BOLD, 20));
g.drawString(""+philosopherId, xPositionPlate+plateRadius-5, yPositionPlate+plateRadius+10 );
if (table.getPhilosopher(philosopherId).getState() == EATING) {
teta = (-phi / 7);
}
if (table.getPhilosopher(table.leftNeighbourId(philosopherId)).getState() == EATING) {
teta = phi / 7;
}
tetaIn = teta * 1.75;
int xStickInner = (int) Math.round(centerX + (tableRadius - halfStickLength) * Math.cos(philosopherId * phi + phi / 2 + tetaIn));
int yStickInner = (int) Math.round(centerY + (tableRadius - halfStickLength) * Math.sin(philosopherId * phi + phi / 2 + tetaIn));
int xStickOuter = (int) Math.round(centerX + (tableRadius + halfStickLength) * Math.cos(philosopherId * phi + phi / 2 + teta));
int yStickOuter = (int) Math.round(centerY + (tableRadius + halfStickLength) * Math.sin(philosopherId * phi + phi / 2 + teta));
g.drawLine(xStickInner, yStickInner, xStickOuter, yStickOuter);
g.drawString(""+philosopherId, xStickInner, yStickInner);
}
}
public void update(Observable o, Object arg) {
repaint();
}
}
static class ConsoleLogger implements Observer {
public ConsoleLogger(PhilosopherTable table) {
table.addObserver(this);
}
public void update(Observable o, Object arg) {
Philosopher philosopher = arg != null ? (Philosopher) arg : null;
if (philosopher == null) {
System.out.println("Application starting");
return;
}
System.out.println("Philosopher " + philosopher.getId() + " " + getStateString(philosopher));
}
private String getStateString(Philosopher philosopher) {
return switch (philosopher.getState()) {
case EATING -> "starts eating";
case THINKING -> "starts thinking";
case HUNGRY -> "is getting hungry";
};
}
}
}
@@ -0,0 +1,257 @@
package ch.zhaw.prog2.philosopher;
import java.util.Arrays;
import java.util.Observable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static ch.zhaw.prog2.philosopher.ForkManager.ForkState.*;
import static ch.zhaw.prog2.philosopher.PhilosopherTable.PhilosopherState.*;
/**
* PhilosopherTable represent the model.
* It is responsible to create, manage and terminate the {@link Philosopher} threads.
*/
class PhilosopherTable extends Observable {
private final int baseTime;
private final int philosopherCount;
private final Philosopher[] philosophers;
private final ForkManager forkManager;
private volatile boolean running;
private boolean isDeadlock = false;
private ExecutorService philosopherExecutor;
private ScheduledExecutorService watchdogExecutor;
public PhilosopherTable(int philosopherCount, int baseTime) {
this.baseTime = baseTime;
this.philosopherCount = philosopherCount;
this.philosophers = new Philosopher[philosopherCount];
this.forkManager = new ForkManager(philosopherCount, baseTime);
for (int philosopherId = philosopherCount - 1; philosopherId >= 0; philosopherId--) {
philosophers[philosopherId] = new Philosopher(philosopherId);
}
notifyStateChange(null);
System.out.printf("Creating table (%d Philosophers, base time = %dms )%n ...", philosopherCount, baseTime);
}
/**
* Get deadlock state
* @return true if deadlock is detected
*/
boolean isDeadlock() {
return isDeadlock;
}
/**
* Get running state
* @return true if table is in running state
*/
boolean isRunning() {
return running;
}
/**
* Internal method to notify GUI-Observers that the state of a philosopher has changed.
* @param sender philosopher whose state has changed
*/
private synchronized void notifyStateChange(Philosopher sender) {
setChanged();
notifyObservers(sender);
checkNeighbourState(sender);
}
/**
* Internal method to verify that no two neighbouring philosophers can be in state EATING
* @param philosopher philosopher to check neighbour states for
*/
private void checkNeighbourState(Philosopher philosopher) {
if (philosopher != null && philosopher.state == EATING) {
int id = philosopher.id;
PhilosopherState leftState = philosophers[leftNeighbourId(id)].state;
PhilosopherState rightState = philosophers[rightNeighbourId(id)].state;
int eatingNeighbour = leftState == EATING ? leftNeighbourId(id) :
rightState == EATING ? rightNeighbourId(id) :
-1;
if (eatingNeighbour >= 0) {
System.out.println("ILLEGAL STATE: Two neighbouring Philosophers are eating: " + id + " | " + eatingNeighbour);
stop();
}
}
}
/**
* Internal method to check if there is a deadlock
*/
private void checkDeadlock() {
this.isDeadlock = forkManager.areAllForksInState(WAITING) && areAllPhilosophersInState(HUNGRY);
if (isDeadlock) {
System.out.println("DEADLOCK: All Philosophers are starving!!!");
stop();
}
}
/**
* Test if all philosophers are in a specific state. Used to detect deadlocks.
* @param state State of philosopher to test for
* @return true if all philosophers are in the given state, false otherwise
*/
private boolean areAllPhilosophersInState(PhilosopherState state) {
return Arrays.stream(philosophers).allMatch(philosopher -> philosopher.state == state );
}
/**
* Start the philosopher processes
*/
public void start() {
this.running = true;
System.out.println("Start deadlock watchdog ...");
watchdogExecutor = Executors.newSingleThreadScheduledExecutor();
watchdogExecutor.scheduleAtFixedRate(this::checkDeadlock, 2, 2, TimeUnit.SECONDS);
System.out.println("Starting philosophers ...");
philosopherExecutor = Executors.newFixedThreadPool(philosopherCount);
for (Philosopher philosopher : philosophers) {
philosopherExecutor.execute(philosopher);
}
}
/**
* Stop the philosopher processes
*/
public void stop() {
if (running) {
this.running = false;
System.out.println("Stopping deadlock watchdog ...");
watchdogExecutor.shutdown();
System.out.println("Stopping philosophers ...");
philosopherExecutor.shutdownNow();
System.out.println("Final state: \n" + this);
System.out.format("Detected at most %d concurrent Philosophers acquiring forks%n", forkManager.getConcurrentAcquires());
} else {
System.err.println("Stop called while not running.");
}
}
/**
* Get a specific philosopher by id
* @param philosopherId id of philosopher to return
* @return philosopher object for specified id
*/
public Philosopher getPhilosopher(int philosopherId) {
return philosophers[philosopherId];
}
/**
* Return the id of the philosopher sitting to the right of the given philosopher id
* @param philosopherId id of the philosopher
* @return id of the neighbour to the right
*/
public int rightNeighbourId(int philosopherId) {
return (philosopherCount + philosopherId - 1) % philosopherCount;
}
/**
* Return the id of the philosopher sitting to the left of the given philosopher id
* @param philosopherId id of the philosopher
* @return id of the neighbour to the left
*/
public int leftNeighbourId(int philosopherId) {
return (philosopherId + 1) % philosopherCount;
}
@Override
public String toString() {
return "PhilosopherTable { running = " + running +
"\n philosophers = " + Arrays.toString(philosophers) +
"\n " + forkManager +
"\n}";
}
/**
* Possible states of a philosopher
*/
enum PhilosopherState {
THINKING, HUNGRY, EATING
}
/**
* Implementation of the Philosopher as an inner class.
* This class should not be changed!
* All logic for acquiring and releasing forks must be implemented in the {@link ForkManager} class
* The {@link PhilosopherTable#running} variable from the outer class {@link PhilosopherTable} is used to control
* termination of the run loop.
* Also, to notify the observers (GUI) the {@link PhilosopherTable#notifyStateChange(Philosopher)} method of
* the outer class is used.
*/
class Philosopher implements Runnable {
private static final int THINK_TIME_FACTOR = 5;
private static final int EAT_TIME_FACTOR = 1;
private final int id;
private PhilosopherState state = THINKING;
public Philosopher(int id) {
this.id = id;
}
public PhilosopherState getState() {
return state;
}
public long getId() {
return id;
}
private void think() throws InterruptedException {
try {
state = THINKING;
notifyStateChange(this);
Thread.sleep((int) (Math.random() * THINK_TIME_FACTOR * baseTime));
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted thinking - " + e.getMessage());
}
}
private void eat() throws InterruptedException {
try {
state = EATING;
notifyStateChange(this);
Thread.sleep((int) (Math.random() * EAT_TIME_FACTOR * baseTime));
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted eating - " + e.getMessage());
}
}
private void takeForks() throws InterruptedException {
state = HUNGRY;
notifyStateChange(this);
forkManager.acquireForks(id);
}
private void putForks() throws InterruptedException {
state = THINKING; // needed to prevent false positive in checkNeighborState
forkManager.releaseForks(id);
}
@Override
public void run() {
System.out.println("Starting Philosopher " + id);
try {
while (running) {
think();
takeForks();
eat();
putForks();
}
} catch (InterruptedException e) {
System.err.println("Interrupted " + this + " : " + e.getMessage());
}
System.out.println("Stopping Philosopher " + id);
}
@Override
public String toString() {
return "Philosopher {" + "id=" + id + ", state=" + state + "}";
}
}
}
@@ -0,0 +1,85 @@
package ch.zhaw.prog2.philosopher;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
class PhilosopherTest {
private static final int PHILOSOPHER_COUNT = 5;
private static final int BASE_TIME = 75; // milliseconds
private PhilosopherTable table;
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}
@Test
void testPhilosopherTable() {
table = new PhilosopherTable(PHILOSOPHER_COUNT, BASE_TIME);
TableStateObserver tableStateObserver = new TableStateObserver(table);
try {
table.start();
int timePassed = 0;
while (!table.isDeadlock() && timePassed < 55) {
TimeUnit.SECONDS.sleep(5);
timePassed += 5;
}
} catch (InterruptedException e) {
fail("Table interrupted", e);
} finally {
table.deleteObserver(tableStateObserver);
assertFalse(table.isDeadlock(),"Deadlock detected: " + table);
table.stop();
}
}
static class TableStateObserver implements Observer {
final static boolean VERBOSE = false;
final PhilosopherTable table;
public TableStateObserver(PhilosopherTable table) {
this.table = table;
table.addObserver(this);
}
public void update(Observable o, Object arg) {
PhilosopherTable.Philosopher philosopher = arg != null ? (PhilosopherTable.Philosopher) arg : null;
if (VERBOSE) printState(philosopher);
if (table.isDeadlock()) {
fail("Deadlock detected: " + table);
} else if (!table.isRunning()) {
fail("Table stopped for other reason: " + table);
}
}
private void printState(PhilosopherTable.Philosopher philosopher) {
if (philosopher == null) {
System.out.println("Application starting");
return;
}
System.out.println("Philosopher " + philosopher.getId() + " " + getStateString(philosopher));
}
private String getStateString(PhilosopherTable.Philosopher philosopher) {
return switch (philosopher.getState()) {
case EATING -> "starts eating";
case THINKING -> "starts thinking";
case HUNGRY -> "is getting hungry";
};
}
}
}