Worksheet: J6 | CS 2113 Software Engineering - Fall 2024

Worksheet: J6

Worksheets are self-guided activities that reinforce lectures. They are not graded for accuracy, only for completion. They are due, on github by 11:59pm on the day of the lecture.

Submit a file called worksheet-J6.md in your repo for this assignment. Submit a file called worksheet-J6.md in your repo for this assignment.

Note

Attempt to answer these questions before running the code. This will improve your ability to analyize and reason about code without an IDE or compiler. This skill we be helpful on the exams.

Questions

  1. Compile the two files below and run the main method in the SimpleThreadExample.

    $ javac *.java
    $ java SimpleThreadExample
    
    /* SimpleThreadExample.java */
    public class SimpleThreadExample {
    
        public static void main(String[] args) {
        
            
            Messages msg = new Messages();
            Thread morningThread = new GreetingsThread(msg, 0);
            Thread afternoonThread = new GreetingsThread(msg, 1);
    
            morningThread.start();
            afternoonThread.start();
            //MARK
            
        }
    }
    
    /* Messages.java */
    public class Messages {
        public String morning;
        public String afternoon;
        public String evening;
        public String night;
    
        public Messages() {
          morning = "Good morning!";
          afternoon = "Good afternnon!";
          evening = "Good evening!";
          night = "Good night!";
        }
    
        public String getMessage(int choice) {
          switch(choice) {
             case 0:
               return this.morning;
             case 1:
               return this.afternoon;
             case 3:
               return this.evening;
             case 4:
               return this.night;
             default:
               return "";        
          }
        }
     }
    
    /* GreetingsThread.java */
    public class GreetingsThread extends Thread {
    
        private Messages msg;
        private int choice;
        public GreetingsThread(Messages msg, int choice) {
            this.msg = msg;
            this.choice = choice;
        }
    
        public void run() {
            System.out.println("Started the " + msg.getMessage(choice) + " thread...");
                
            //print 10 times
            for (int i = 0; i < 10; i++) {
                System.out.println(msg.getMessage(choice));
            }
    
            System.out.println("Exiting the " + msg.getMessage(choice) + " thread...");
        }
    }
    

    Then:

    • Run the program a few times. Describe the output of this program. Is it consistent?
    • Draw a memory diagram of the program at MARK

  2. Now let’s modify GreetingsThread to add a Thread.sleep() in the run() method. Recompile the GreatingThread class as below.

    Run the program multiple times. Does the output change in any way? Does one thread always finish first, or does the order change?

    /* GreetingsThread.java */
    public class GreetingsThread extends Thread {
        private Messages msg;
        private int choice;
    
        public GreetingsThread(Messages msg, int choice) {
            this.msg = msg;
            this.choice = choice;
        }
    
        public void run() {
            System.out.println("Started the " + msg.getMessage(choice) + " thread...");
    
            for (int i = 0; i < 10; i++) {
                System.out.println(msg.getMessage(choice));
    
                try {
                    // Sleep for 1 second (1000 milliseconds)
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    System.out.println("Interrupted while sleeping...");
                }
            }
    
            System.out.println("Exiting the " + msg.getMessage(choice) + " thread...");
        }
    }
    

  3. Now let’s add a System.out.println() at the end of the main method. Recompile the program with this addition, continuing from above. Explain how it is possible that the main method is complete but the program is still producing output.

    /* SimpleThreadExample.java */
    public class SimpleThreadExample {
        public static void main(String[] args) {
                
            Messages msg = new Messages();
            Thread morningThread = new GreetingsThread(msg, 0);
            Thread afternoonThread = new GreetingsThread(msg, 1);
    
            morningThread.start();
            afternoonThread.start();
    
            System.out.println("Main method exiting...");
        }
    }
    

  4. Finally let’s add a thread.join() to join the morningThread for 5 seconds before starting the afternoonThread. Recompile and rerun. Then describe the output of this program. Explain how attempting to join the first thread for 5 seconds affects the output of this program.

    /* SimpleThreadExample.java */
    public class SimpleThreadExample {
        public static void main(String[] args) {
            Messages msg = new Messages();
            Thread morningThread = new GreetingsThread(msg, 0);
            Thread afternoonThread = new GreetingsThread(msg, 1);
    
            morningThread.start();
    
            try {
                System.out.println("Joining the morning thread for 5 seconds...");
                morningThread.join(5000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted while joining a thread...");
            }
    
            afternoonThread.start();
    
            System.out.println("Main method exiting...");
        }
    }
    

  5. What does join() do compared to join(10)?

  6. What is the difference between isAlive() and join()? (note, no arguments to join.)

  7. Compile the two files below and run the main method in the AnimalFootRace. Then describe the output of this program. Explain why the threads finish in the order that they do.

    $ javac *.java
    $ java AnimalFootRace
    
    /* AnimalFootRace.java */
    public class AnimalFootRace {
        public static void main(String[] args) {
            Thread tortoiseThread = new AnimalRacerThread("Tortoise", 5);
            Thread hareThread = new AnimalRacerThread("Hare", 20);
            Thread cheetahThread = new AnimalRacerThread("Cheetah", 50);
    
            System.out.println("On your marks, get set, go!");
            tortoiseThread.start();
            hareThread.start();
            cheetahThread.start();
        }
    }
    
    /* AnimalRacerThread.java */
    public class AnimalRacerThread extends Thread {
        public static final int NUM_LAPS = 8;
        private String animalName;
        private int animalSpeed;
    
        public AnimalRacerThread(String animalName, int animalSpeed) {
            this.animalName = animalName;
            this.animalSpeed = animalSpeed;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= NUM_LAPS; i++) {
                System.out.println(animalName + " lap " + i);
                try {
                    Thread.sleep(1000 / animalSpeed);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted while sleeping...");
                }
            }
            System.out.println(animalName + "Finished!");
        }
    }
    

  8. Modify the AnimialFootRace main method. Add additional code to give the hare a head start, without modifying the animal speeds. Then compile the two files below and run the main method in the AnimalFootRace. Provide the modified code below. Then describe the output of this program. Explain why your modification worked or did not work.

    $ javac *.java
    $ java AnimalFootRace
    
    /* AnimalFootRace.java */
    public class AnimalFootRace {
        public static void main(String[] args) {
            Thread tortoiseThread = new AnimalRacerThread("Tortoise", 5);
            Thread hareThread = new AnimalRacerThread("Hare", 20);
            Thread cheetahThread = new AnimalRacerThread("Cheetah", 50);
    
            System.out.println("On your marks, get set, go!");
            tortoiseThread.start();
            hareThread.start();
            cheetahThread.start();
        }
    }
    
    /* AnimalRacerThread.java */
    public class AnimalRacerThread extends Thread {
        public static final int NUM_LAPS = 8;
        private String animalName;
        private int animalSpeed;
    
        public AnimalRacerThread(String animalName, int animalSpeed) {
            this.animalName = animalName;
            this.animalSpeed = animalSpeed;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= NUM_LAPS; i++) {
                System.out.println(animalName + " lap " + i);
                try {
                    Thread.sleep(1000 / animalSpeed);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted while sleeping...");
                }
            }
            System.out.println(animalName + "Finished!");
        }
    }
    

  9. Modify the AnimialFootRace main method. Add a new animal thread to the foot race. Then compile the two files below and run the main method in the AnimalFootRace. Provide the modified code below. Then describe the output of this program.

    $ javac *.java
    $ java AnimalFootRace
    
    /* AnimalFootRace.java */
    public class AnimalFootRace {
        public static void main(String[] args) {
            Thread tortoiseThread = new AnimalRacerThread("Tortoise", 5);
            Thread hareThread = new AnimalRacerThread("Hare", 20);
            Thread cheetahThread = new AnimalRacerThread("Cheetah", 50);
    
            System.out.println("On your marks, get set, go!");
            tortoiseThread.start();
            hareThread.start();
            cheetahThread.start();
        }
    }
    
    /* AnimalRacerThread.java */
    public class AnimalRacerThread extends Thread {
        public static final int NUM_LAPS = 8;
        private String animalName;
        private int animalSpeed;
    
        public AnimalRacerThread(String animalName, int animalSpeed) {
            this.animalName = animalName;
            this.animalSpeed = animalSpeed;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= NUM_LAPS; i++) {
                System.out.println(animalName + " lap " + i);
                try {
                    Thread.sleep(1000 / animalSpeed);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted while sleeping...");
                }
            }
            System.out.println(animalName + "Finished!");
        }
    }
    

  10. The Singleton class below implements the Singleton pattern. The singleton pattern is a software engineering design pattern that restricts the instantiating of a class to a single instance. Review the Singleton class code below. Is it possible to create more than one instance of the Singleton class if two threads attempt to call the getInstance() method at the same time?

    // Java program to create a Singleton class.
    public class Singleton {
        // This is a private member variable so that the singletonInstance
        // can only be accessed through the getInstance() method.
        private static Singleton singletonInstance;
    
        // Private constructor forces the class to be instantiated 
        // via the getInstance method.
        private Singleton() {
            // private constructor.
        }
    
        // Method to get an instance of this class.
        public static Singleton getInstance() {
            // If this singleton instance is null, 
            // then construct a new instance.
            // Otherwise return the existing instance.
            if (singletonInstance == null) {
                singletonInstance = new Singleton();
            }
    
            return singletonInstance;
        }
    }
    

  11. Review the modified thread safe Singleton class code below. Is it possible to create more than one instance of the Singleton class if two threads attempt to call the getInstance() method at the same time? How does the synchronized keyword affect the attempts to call the getInstance method from multiple threads at the same time?

    // Java program to create thread safe Singleton class.
    public class Singleton {
        // This is a private member variable so that the singletonInstance
        // can only be accessed through the getInstance() method.
        private static Singleton singletonInstance;
    
        // Private constructor forces the class to be instantiated 
        // via the getInstance method.
        private Singleton() {
            // private constructor.
        }
    
        // Synchronized method to control simultaneous access 
        // to the getInstance method.
        synchronized public static Singleton getInstance() {
            // If this singleton instance is null, 
            // then construct a new instance.
            // Otherwise return the existing instance.
            if (singletonInstance == null) {
                singletonInstance = new Singleton();
            }
    
            return singletonInstance;
        }
    }
    

  12. Review the ThreadRunner class code below. Compile and run the ThreadRunner main method. Describe the output. Does the execution halt? Does SimpleThreadTwo finish running? If not, why does SimpleThreadTwo get stuck in the while loop (hint: recall the compiler optimization example)?

    $ javac ThreadRunner.java
    $ java ThreadRunner
    
    /* ThreadRunner.java */
    public class ThreadRunner {
    
        private static boolean statusFlag = false;
    
        private static class SimpleThreadOne extends Thread {
            public void run() {
                for (int i = 1; i <= 2000; i++){
                    System.out.println("Simple thread one counter - " + i);
                }
                // Change the status flag.
                statusFlag = true;
                System.out.println("Status flag changed to true in simple thread one.");
            }
        }
    
        private static class SimpleThreadTwo extends Thread {
            public void run() {
                int waitCounter = 1;
                while (!statusFlag) {
                    waitCounter++;
                }
                System.out.println("Start simple thread two processing " + waitCounter);
            }
        }
    
        public static void main(String[] args) {
            SimpleThreadOne simpleThreadOne = new SimpleThreadOne();
            simpleThreadOne.start();
            SimpleThreadTwo simpleThreadTwo = new SimpleThreadTwo();
            simpleThreadTwo.start();
        }
    }
    
    

  13. Review the modified ThreadRunner class code below. Compile and run the ThreadRunner main method. Describe the output. Does the execution halt? Does SimpleThreadTwo finish running? If it does finish running, why does adding the volatile keyword before the boolean statusFlag change the behavior of the code compared to the previous question?

    $ javac ThreadRunner.java
    $ java ThreadRunner
    
    /* ThreadRunner.java */
    public class ThreadRunner {
    
        private static volatile boolean statusFlag = false;
    
        private static class SimpleThreadOne extends Thread {
            public void run() {
                for (int i = 1; i <= 2000; i++){
                    System.out.println("Simple thread one counter - " + i);
                }
                // Change the status flag.
                statusFlag = true;
                System.out.println("Status flag changed to true in simple thread one.");
            }
        }
    
        private static class SimpleThreadTwo extends Thread {
            public void run() {
                int waitCounter = 1;
                while (!statusFlag) {
                    waitCounter++;
                }
                System.out.println("Start simple thread two processing " + waitCounter);
            }
        }
    
        public static void main(String[] args) {
            SimpleThreadOne simpleThreadOne = new SimpleThreadOne();
            simpleThreadOne.start();
            SimpleThreadTwo simpleThreadTwo = new SimpleThreadTwo();
            simpleThreadTwo.start();
        }
    }
    
    

  14. A second price auction is an auction where the highest bidder only has to pay whatever the second highest bid was. For example if person A bids $1 and person B bids $2, then person B wins the auction, but only pays $1. Below are two classes that reflect this, SecondPriceAuction and Bidder.

    Review the code below. What might happen if two threads call the makeBid method at the same time? How would you modify the code to protect the auction outcome?

    public class SecondPriceAuction {
        private int currentHighestBid = 0;
        private int secondHighestBid = 0;
    
        public int getSecondHighestBid() {
            return secondHighestBid;
        }
    
        public void makeBid(int amount) {
            if (amount > currentHighestBid) {
                secondHighestBid = currentHighestBid;
                currentHighestBid = amount;
            }
            else if(amount > secondHighestBid) {
                secondHighestBid = amount;
            }
        }
    }
    
    import java.util.Random;
    
    public class Bidder extends Thread {
        private SecondPriceAuction secondPriceAuction;
        private int maxBid;
    
        public Bidder(SecondPriceAuction secondPriceAuction, int maxBet) {
            this.secondPriceAuction = secondPriceAuction;
            this.maxBid = maxBet;
        }
    
        public void run() {
            try {
                Thread.sleep((new Random()).nextInt(100));
            } catch (InterruptedException e) {
                System.out.println("Interrupted while sleeping...");
            }
    
            secondPriceAuction.makeBid(maxBid);
        }
    }
    
    public class AuctionRunner {
        public static void main(final String args[]) {
            SecondPriceAuction secondPriceAuction = new SecondPriceAuction();
            Bidder[] bidders = new Bidder[3];
    
            // Create new bidders.
            for (int i = 0; i < bidders.length; i++) {
                bidders[i] = new Bidder(secondPriceAuction, i + 1);
            }
    
            // Start bidding.
            for (int i = 0; i < bidders.length; i++) {
                bidders[i].start();
            }
    
            // Ensures all threads have finished before we print out the price
            for (Bidder bidder : bidders) {
                try {
                    bidder.join();
                } catch (InterruptedException e) {
                    System.out.println("Interrupted while joining a thread...");
                }
            }
    
            System.out.println("Final Price: $" + secondPriceAuction.getSecondHighestBid());
        }
    }