Strengths and Weaknesses of Threads

ProCon
  • Take advantage of multiple processors
  • Use time efficiently when program is blocked for IO
  • Allows a client to access multiple remote servers simultaneously
  • Allows a server to handle multiple clients simultaneously
  • Added code complexity
  • Additional overhead
  • Dealing with concurrency issues including Race Conditions and Deadlock

Concurrency

Concurrent programming is when a single program can perform multiple funcitons in parallel. Java Threads allow concurrency.

Threads can help speed up our code quite a bit. One of the advantages of threads is that they can access and alter shared resources, while acting in parallel. However, this can also lead to heretofore unseen bugs.

Since the threads do not communicate, there is no built-in mechanism to prevent them from interfering from each other. It is the responsibility of the programmer to put safeguards in the code to avoid this type of problems.

Issues with Threads: Race Conditions

A Race condition is a bug in a program where the output of the program is dependent upon the timing the OS chooses in running threads. Consider the case where two threads, T1 and T2, are accessing a shared int a, which equals 0, and incrementing it. From your architecture class, you know the following steps need to be completed:

  1. T1 fetches the value of a from memory into register1: 0
  2. T1 increments the value of a in register1: 1
  3. T1 stores the value of register1 in memory: 1
  4. T2 fetches the value of a from memory into register2: 1
  5. T2 increments the value of a in register2: 2
  6. T2 stores the value of register2 in memory: 2

At the end of these steps, we expect that a==2. It started as 0, and two threads incremented it, so it SHOULD have the value 2. However, the OS could also choose to schedule these steps this way:

  1. T1 fetches the value of a from memory into register1: 0
  2. T2 fetches the value of a from memory into register2: 0
  3. T1 increments the value of a in register1: 1
  4. T1 stores the value of register1 in memory: 1
  5. T2 increments the value of a in register2: 1
  6. T2 stores the value of register2 in memory: 1

This time, a==1.

For a more concrete example, download RaceThread and Shared. You'll notice that sometimes it outputs an X, and sometimes outputs a period. This is due to the fact that calling bump() in one thread, is sometimes interrupted by the call of diff() in the other thread. More surprisingly, if you run this twice, you will not get the same results back!

Race conditions are nasty bugs that have led to the near-loss of the Spirit Mars rover, and the North American blackout of 2003.

Issues with Threads: Deadlock

Deadlock When it is very important that race conditions not occur, shared resources are sometimes reserved by a thread. For example, all over the world, people are making transactions using their bank accounts. Consider a couple that share a joint bank account. If both are allowed to change their bank account simultaneously, we have a dangerous race condition. So, consider the following pseudocode for transferring money from account A to account B:

  transfer(Account A, Account B, Money m):
    when A is available:
      reserve A
    when B is available:
      reserve B
    A = A - m
    B = B + m
    release B
    release A

Great! No more possible race condition! Only one person can alter a given bank account at a time. However, now consider the situation where one person tries to transfer money from Account A to Account B, at the same time that another tries to transfer money from B to A. One thread reserves the first account, and the other reserves the second. Now, neither thread can make progress: they have encountered deadlock, and both processes stop.

It is not the purpose of this course to address solutions to these concurrent programming issues, but it is important to be aware of them and know their definitions.

New modifiers associated with threading

volatile - Applies to variables. Indicates that a variable might be modified by multiple threads.

It is not necessary to use volatile, but it is a good idea. The compiler will often optimize code, such as telling the CPU to store a cached value. volatile ensures that the compiler always refers to the actual variable in memory.

e.g. public static volatile boolean done = false;

synchronized - applies to methods. Indicates that a method may be run by multiple threads on the same data object. When a synchronized method starts, the interpreter creates a block on the data object. No other thread may access the data until the synchronized method completes. This will prevent the problem described in Race Conditions above.

public class ThreadedCounter extends Thread {
    private int value = 0;
    public synchronized void increment() {
        value++;
    }
    public synchronized void decrement() {
        value++;
    }
    public synchronized int getValue() {
        return value;
    }
}

Note that synchronized is not a panacea. You may not want to use it on methods that take a long time to complete. By blocking the other threads, you are losing the speed advantage that you gain by threading.