Using Java to deal with multicore programming complexity: Part 1 - How Java eases multicore hardware demands on software

Kelvin Nilsen, Atego

June 24, 2012

Kelvin Nilsen, Atego


A motivating example
The Java language was designed by Sun Microsystems during the early 1990s and released to the general public in 1995. At the time, Sun Microsystems had already made the shift to multiprocessor architectures. It would appear that Sun Microsystems felt it was important that Java improve the ease with which multiprocessor software could be developed and maintained, as various multiprocessing features were designed into the Java language. These features integrate at all levels of the Java implementation, manifesting themselves within the programming language syntax and source compilers, in special byte codes and constraints on implementation of the Java virtual machine, and throughout the standard libraries. Unlike C and C++, Java provides a strong foundation for building efficient, portable, and scalable applications and software components that fully exploit whatever multiprocessing capabilities are provided by the underlying hardware.

The concurrency features of Java are defined to provide compatible behavior on both uniprocessors and multiprocessors. On uniprocessors, concurrency is implemented by time slicing and priority preemption. On multiprocessors, multiple concurrent threads execute in parallel on different processor cores. Below, we describe the behavior of a simple concurrent Java program.

The thread type. A Java application is comprised in general of one or more independent threads of execution. Each thread represents an independent sequence of instructions. All threads within a particular Java application can see the same memory. In the vernacular of POSIX [12], all of the threads running within a single Java virtual machine reside within the same process.

Defining a new thread in Java is straightforward. Since Java is an object-oriented language, the developer simply introduces a new Class extending the java.lang.Thread class, overriding the run() method with the body of code to be executed within this thread. See the excerpt below for an example.

  public class MyThread extends java.lang.Thread {
    final private String my_id;

    // constructor
    public MyThread(String name) {
      my_id = name;
    }

    public void run() {
      int count = 0;
      while (true) {
        System.out.println(“MyThread “ + my_id + “ has iterated “ + count + “ times”);
        count++;
      }
    }
  }


To spawn this thread’s execution, the program simply instantiates a MyThread object and invokes its start() method. This is demonstrated in the following code sequence.

  class MyMain {
    public static void main(String[] thread_names) {
      for (int i = 0; i < thread_names.length; i++) {
        MyThread a_thread = new MyThread(thread_names[i]);
        a_thread.start();
      }
    }
  }


For an invocation of this main program with command-line arguments bob fred sally, the first several lines of output were:

  MyThread bob has iterated 0 times
  MyThread bob has iterated 1 times
  MyThread bob has iterated 2 times
  MyThread bob has iterated 3 times
  MyThread bob has iterated 4 times
  MyThread fred has iterated 0 times
  MyThread sally has iterated 0 times
  MyThread bob has iterated 5 times
  MyThread fred has iterated 1 times
  MyThread sally has iterated 1 times
  MyThread bob has iterated 6 times
  ...


Synchronized statements.
Whenever multiple threads share access to common data, it is occasionally necessary to enforce mutual exclusion for certain activities, enforcing that only one thread at a time is involved in those activities. Consider, for example, a situation where one thread updates a record representing the name and phone number of the person on-call to respond to any medical emergencies that might arise. Many other threads might consult this record in particular situations. An (incorrect) implementation of this record data abstraction is shown below:

  public class OnCallRecord {
    private String name, phone;

    public void overwrite(String new_name, String new_phone) {
      name = new_name;
      // placing print statement here increases probability that we’ll experience a context switch after incompletely updating record
      System.out.println(“overwriting OnCallRecord with: “ + new_name + “, phone: “ + new_phone);
      phone = new_phone;
    }

    public OnCallRecord clone() {
      OnCallRecord copy = new OnCallRecord();
      copy.name = this.name;
      copy.phone = this.phone;
      return copy;
    }

    public String name() {
      return name;
    }

    public String phone() {
      return phone;
    }
  }

The problem with the above code is that the overwrite() method may be preempted before the entire record has been updated. If another thread performs a clone() during this preemption, the other thread will see inconsistent data, perhaps obtaining one person’s name paired with a different person’s phone number. An excerpt of an execution trace for an actual run involving one updating thread and one inquiring thread shows that this possibility is real. In this run, the initial value of the OnCallRecord had name George, with phone 111-2222.

  [1] overwriting OnCallRecord with: Mary, phone: 222-3333
  [2] Responsible party is: Mary, phone: 111-2222
  [3] overwriting OnCallRecord with: Mark, phone: 444-5555
  [4] Responsible party is: Mark, phone: 222-3333
  [5] overwriting OnCallRecord with: Sally, phone: 666-7777
  [6] Responsible party is: Sally, phone: 444-5555
  [7] Responsible party is: Sally, phone: 444-5555
  [8] Responsible party is: Sally, phone: 444-5555
  [9] Responsible party is: Sally, phone: 444-5555
  [10] Responsible party is: Sally, phone: 444-5555
  [11] overwriting OnCallRecord with: Sue, phone: 888-9999
  [12] Responsible party is: Sue, phone: 666-7777

Note that, for this execution trace, the phone number is consistently wrong. The fix for this problem is to add the synchronized keyword to each of the methods in the definition of OnCallRecord.

  public class OnCallRecord {
    private String name, phone;

    public synchronized void overwrite(String new_name, String new_phone) {
      name = new_name;
      System.out.println(“overwriting OnCallRecord with: “ + new_name + “, phone: “ + new_phone);
      phone = new_phone;
    }

    public synchronized OnCallRecord clone() {
      OnCallRecord copy = new OnCallRecord();
      copy.name = this.name;
      copy.phone = this.phone;
      return copy;
    }

    public synchronized String name() {
      return name;
    }

    public synchronized String phone() {
      return phone;
    }
  }

With this change, a similar execution trace shows much better behavior:

  [1] overwriting OnCallRecord with: Bob, phone: 123-4567
  [2] Responsible party is: Bob, phone: 123-4567
  [3] overwriting OnCallRecord with: Nancy, phone: 321-9876
  [4] Responsible party is: Nancy, phone: 321-9876
  [5] overwriting OnCallRecord with: George, phone: 111-2222
  [6] Responsible party is: George, phone: 111-2222
  [7] Responsible party is: George, phone: 111-2222
  [8] Responsible party is: George, phone: 111-2222
  [9] Responsible party is: George, phone: 111-2222
  [10] Responsible party is: George, phone: 111-2222
  [11] overwriting OnCallRecord with: Mary, phone: 222-3333
  [12] Responsible party is: Mary, phone: 222-3333
  [13] overwriting OnCallRecord with: Mark, phone: 444-5555
  [14] Responsible party is: Mark, phone: 444-5555
  [15] overwriting OnCallRecord with: Sally, phone: 666-7777
  [16] Responsible party is: Sally, phone: 666-7777


< Previous
Page 2 of 4
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER