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


Java’s architecture-agnostic concurrency features
In what should be of particular interest to developers looking to migrate their software code from uniprocessors to multiprocessors, the concurrency features of Java are defined to provide compatible behavior in both architectural environments.

On uniprocessors, concurrency is implemented by time slicing and priority preemption. On multiprocessors, multiple concurrent threads execute in parallel on different processor cores with few problems if the developer has a good understanding of the key concurrency constructs built into the Java platform, including those related to thread types, synchronized statements, wait and notify, and volatile variables. Numerous programming aids are also available in the form of extensive concurrency libraries.

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 [Reference source not found.], 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();
      }
    }
  }


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 an illustrative 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. 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;
    }
  }


One effect of adding the synchronized qualifier to a method is to cause the Java compiler to insert code into the method’s prologue to acquire a mutual exclusion lock associated specifically with the object to which the method is attached. Code to release the mutual exclusion lock is inserted into the method’s epilogue. Some comparisons with C (and C++) are appropriate:
  1. Note that the implementor of the OnCallRecord has declared the name and phone fields to be private. This encapsulates access, assuring that the only way to modify or view the values of these fields is by means of the public methods associated with this data type. Thus, the implementor of OnCallRecord can assure that access to its internal data is properly synchronized. The Java compiler enforces that there is no way for other code outside the OnCallRecord abstraction to violate these constraints.
  2. As a high-level programming abstraction, much of the low-level detail associated with managing synchronization locks is hidden from developers. The Java virtual machine automatically decides when to allocate and deallocate lock data structures and has full control over when and how to coordinate with the underlying operating system.
  3. Since the synchronized keyword applies to entire methods and statements, it is block oriented. The Java compiler enforces that the lock is acquired upon entry to the block and is released upon exit from the block. Unlike C, there is no way for a programmer to forget to release the lock. The Java platform assures that the lock will be released even if this block of code is exited abnormally, such as when an exception is thrown or a break statement leaves an inner-nested loop.
  4. Since it is known by the Java run-time environment that this lock is associated with mutual exclusion and nested locks are released in LIFO order, the Java run-time is able to reliably implement priority inheritance and even priority ceiling emulation for the synchronization locks. Priority inversion avoidance is not required by the Java language specification, but it is implemented by several real-time virtual machines.
  5. Because the synchronize keyword is built into the language itself, the compiler and run-time environment are able to cooperate in providing efficient implementations. The typical implementation of Java lock and unlock services is much more efficient than the RTOS system calls that are required to implement these services in typical C code.
< Previous
Page 3 of 4
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER