Level: Introductory Peter Haggar (haggar@us.ibm.com)IBM
01 Oct 2001 This technical article was originally published a previous issue of the IBM DeveloperToolbox Technical Magazine. Subscribe to the IBM developerWorks journal for the latest technical articles on open standards-based technologies that are available to you offline in a printed publication.
The Java programming language has matured, and there are many programmers proficient with it.
Unfortunately, there remain many programmers that make the wrong choices and assumptions about how the
Java platform operates. This article lists several myths that programmers continue to embrace. It reveals
each myth, offers an explanation on why the myth is perpetuated, discusses why it is wrong, and explores the facts. Myth 1: Garbage collection solves all memory-related problems
Garbage collection is a big help to programmers.
Not having to worry as much about memory issues is a big advantage for using the Java programming language.
However, the fact that the garbage collector cleans up unused memory is not a reason to avoid thinking about
memory issues. There are areas that, if ignored, cause trouble.
First, garbage collection algorithms vary
from virtual machine (VM) to VM. Therefore, you cannot depend on particular garbage collection behavior
across different VM systems. Garbage collection is an active area of research; better, faster, and more
accurate collectors are implemented all the time.
Most modern garbage collectors, however, share a number
of things in common. One is that when they run, they do not always free all objects that are available
for collection. Analysis has shown that most objects in Java programs are short lived. Therefore, the
collector, in an effort to be more efficient, examines long-lived objects less frequently than short-lived
objects. The thinking is that because most objects are short lived, an object that has been referenced
for a longer time will probably be referenced later. Consequently, there is no need to spend time on
each garbage collection invocation checking to see if the object is available to be reclaimed.
It may
take multiple invocations of the garbage collector to free a particular object's memory. You can suggest
that the garbage collector run by calling the System.gc method. Invocation of this method usually results in the garbage
collector doing a full collection. Normally, this is a more exhaustive and complete process than when the
collector is invoked by the VM itself. The rationale for this is when the VM invokes the collector, it should
finish as soon as possible. If a programmer explicitly calls System.gc, the inference is that there is more
time available to do the most amount of work. In either case, do not assume that all available objects are
collected. There is a better chance that all available objects are collected when the garbage collector is
invoked due to an explicit call to System.gc, but there is no guarantee.
Another area where programmers can
get into trouble is when they hold on to object references that are not needed. This impedes the garbage
collector's ability to free object memory. This situation can occur when you manage your own list.
Consider the
ObjStack class in Listing 1. This class has push and pop methods to manage the stack of objects. Both methods
utilize an index that is used to indicate the next available slot in the stack. The push method stores a
reference to the new object and increments the index, while the pop method decrements the index and returns
the top element from the stack.
class ObjStack
{
private Object[] stack;
private int index;
public void push(Object o)
{
stack[index] = o;
index++;
}
public Object pop()
{
index-;
return stack[index];
}
//...
}
|
Now, create an object of this class with a ten-element stack,
and call the push method eight times to add objects, A through H. The stack looks like this: 
Now, consider
what happens when you call the pop method three times to retrieve the top three elements off the stack. The
stack now looks like this:
Note that the stack has not changed other than the index field has been
decremented. References to objects F, G, and H have been returned from the pop method as expected. However,
the stack still holds references to them. If the code calling the pop method dereferences F, G, and H, the calling
code has every expectation that these objects are available for garbage collection, assuming there are no other
references to them in the application. However, the stack class used to store these objects has retained a
reference to each of them. These objects are never reclaimed unless another object is pushed onto the stack in
its place. The correct implementation of the pop method for the stack class is as follows:
public Object pop()
{
index-;
Object o = stack[index];
stack[index] = null;
return o;
}
|
In this version of the pop method, when
references are returned, the stack dereferences them and thus removes extraneous references to the popped
objects that could impede garbage collection in the future. Using this new version of the pop method with
the code from Listing 1 results in a stack that looks like this:
Your code should not hold onto
references that are no longer needed. The amount of memory that is in use can adversely affect the performance
of the application. The less memory available often requires the garbage collector to run more often,
hampering performance.
Myth 2: Parameters are passed by reference
The pass-by-value versus pass-by-reference
argument is a frequently debated topic in various Java newsgroups. The central point of confusion lies in the
following two facts of the Java programming language:
- Objects are passed by reference
- Parameters are passed by value
Can these two statements be true at the same time? In a word: Yes. You never pass objects
in the Java programming language; you pass references. Therefore, in one sense, the Java programming
language is pass by reference. However, when you pass a parameter, it uses only one parameter-passing
mechanism, and that is pass by value. Normally, when programmers talk about pass by value or pass by
reference, they refer to the parameter-passing mechanism of a language. The C++ language supports both
mechanisms, and former C++ programmers initially seem to be the most unsure about how Java programming passes parameters. Java
programming only supports the pass-by-value parameter-passing mechanism and, once understood, makes things
simpler. Variables in Java programming can be one of two types: reference types or primitive types. Both types
are handled the same way when passed as arguments to a method. Both are passed by value; neither is passed by
reference. This is an important distinction as the subsequent code examples illustrate.
Before going further,
it is important to define the terms pass by value and pass by reference. The term pass by value means that
when an argument is passed to a function, the function receives a copy of the original value. Therefore, if
the function modifies the parameter, only the copy is changed and the original value remains unchanged. The
term pass by reference means that when an argument is passed to a function, the function receives the memory
address of the original value, not a copy of the value. Therefore, if the function modifies the parameter,
the original value in the calling code is changed.
Some of the confusion about parameter passing in Java
programming originates from the fact that many programmers came to Java programming from C++. The C++
language contains both non-reference types and reference types and passes them by value and by reference,
respectively. The Java programming language has primitive types and object references; and, therefore, it
is logical to think that Java programming uses pass by value for the primitive types and pass by reference
for references, much like C++. After all, you might think if you are passing a reference, it must be pass
by reference. It is tempting to believe this, but it is not true.
In both C++ and Java programming, when a
parameter to a function is not a reference, you pass a copy of the value (pass by value). The difference is
with references. In C++, when a parameter to a function is a reference, you pass the reference or the
memory address (pass by reference). In Java programming, when an object reference is a parameter to a method, you pass a
copy of the reference (pass by value), not the reference itself. This is an important distinction. Note that
Java programming does nothing differently when passing parameters of varying types like C++ does. Java
programming passes all parameters by value, thus making copies of all parameters regardless of type.
Still not
convinced? If Java programming passed parameters by reference, the swap methods in Listing 2 would swap their
arguments. Because it passes all parameters by value, these swap methods do not work as intended.
class Swap
{
public static void main(String args[])
{
Integer a, b;
int i,j;
a = new Integer(10);
b = new Integer(50);
i = 5;
j = 9;
System.out.println("Before Swap, a is " + a);
System.out.println("Before Swap, b is " + b);
swap(a, b);
System.out.println("After Swap a is " + a);
System.out.println("After Swap b is " + b);
System.out.println("Before Swap i is " + i);
System.out.println("Before Swap j is " + j);
swap2(i,j);
System.out.println("After Swap i is " + i);
System.out.println("After Swap j is " + j);
}
public static void swap(Integer ia, Integer ib)
{
Integer temp = ia;
ia = ib;
ib = temp;
}
public static void swap2(int li, int lj)
{
int temp = li;
li = lj;
lj = temp;
}
}
|
The output of the code in Listing 2 is:
Before Swap, a is 10
Before Swap, b is 50
After Swap a is 10
After Swap b is 50
Before Swap i is 5
Before Swap j is 9
After Swap i is 5
After Swap j is 9
|
Figures 1 through 4 show exactly what happens in the code in Listing 2 with regard to the
references. Figure 1 shows the state of the object references and the heap before calling the
swap method. Figure 2 shows the object state after calling the swap method but before it
executes. Figure 3 shows the object state after the swap method executes but before it returns.
Figure 4 shows the object state after the swap method returns.
Figure 1. Before calling method swap

Figure 2. After calling method swap but before it executes

Figure 3. After method swap executes but before it returns

Figure 4. After method swap returns

Because the swap method
received a copy of the reference parameters (pass by value), changes to them are not reflected in the calling
code. Remember, Java programming passes object references by value.
Myth 3: Atomic operations are thread safe
It has often been written and said that atomic operations in the Java programming language are thread safe.
Atomic operations are, by definition, operations that occur without interruption leading many to think they
are therefore thread safe. There are, in fact, a number of atomic operations in Java programming, but they are
not necessarily thread safe.
This myth came about initially due to efforts to limit the use of synchronization
in code. Synchronization has a performance penalty, although that penalty varies from VM to VM. In addition,
with the more modern VMs, the efficiency of synchronization is improving. Despite the improvements in
synchronization performance, it still has a cost; and programmers forever try to improve the efficiency of
their code. Therefore, this myth has perpetuated.
In Java programming, assignments of 32 bits or less are
atomic. On 32-bit hardware, all primitive types, except double and long, are typically represented by 32
bits, with double and long being typically represented by 64 bits. In addition, object references, which
are implemented as native pointers, are typically 32 bits as well. Operations with these 32-bit types
are atomic.
These primitive values are typically represented by 32 or 64 bits. This represents another
small myth about Java programming. It is often written and said that the size of primitive types is
guaranteed by the language. This is not true. What is guaranteed by the language is the range of values
for each primitive type, not its storage size inside of the VM. Therefore, an int will always represent
the same range of values. However, one VM implementation might use 32 bits to represent an int, while another uses 64 bits. The only
guarantees are that the range of values it represents is the same across all platforms, and operations on 32
bit or smaller values are atomic.
So, how can atomic operations not be thread safe? The main point is that
they may indeed be thread safe, but there is no guarantee that they are. Java threads are allowed to keep
private copies of variables separate from main memory. This is done as a performance optimization, allowing
a thread to work with a private local copy instead of with main memory each time. Consider the following
class:
class RealTimeClock
{
private int clkID;
public int clockID()
{
return clkID;
}
public void setClockID(int id)
{
clkID = id;
}
//...
}
|
Now, consider one instance of the RealTimeClock class and two
threads calling the setClockID and clockID methods concurrently and the following sequence of events:
- T1 calls setClockID passing 5
- 5 placed in T1's private working memory
- T2 calls setClockID passing 10
- 10 placed in T2's private working memory
- T1 calls clockID, which returns 5
- 5 returned from T1's private working memory
The call to clockID should return the value 10, because that is what it was
set to by T2. However, 5 is returned, because the read and the write were done to the private working
memory and never to main memory. At no point was the variable reconciled with main memory. The
assignments are certainly atomic; but because of the allowance for this type of behavior in a VM,
they are not necessarily thread safe. There is no guarantee that this problem will occur, but there
is no guarantee that it won't occur either. Figure 5 shows how this problem occurs.
Figure 5. Two threads utilizing private working memory

These two threads have private local copies of the
variables that are not reconciled with the main memory in the heap. When this implementation is
present, the private local variables are reconciled with main memory only under two specific conditions:
- The variable is declared volatile
- The variable is accessed within a synchronized method or block
If the variable is declared volatile, it is reconciled with main memory on each access. This
reconciliation is guaranteed by the language to be atomic, even for 64-bit values. (Note that many VMs may not
implement the volatile keyboard correctly. You can find more information about this on the JavaSoft Web site at
www.javasoft.com.) In addition, if the variable is accessed in a synchronized method or block, it is reconciled
when the lock is obtained, at method or block entry, and when the lock is released, at method or block exit.
Using either solution guarantees that the call to ClockID returns 10, the correct value. The solution you choose
can have performance implications depending on how often the variable is accessed. If concurrency is important
and you are not updating many variables, consider using volatile. If you are updating many variables, however,
using volatile might be slower than using synchronization. Remember that when variables are declared volatile,
they are reconciled with main memory on every access. By contrast, when synchronized is used, the variables are
reconciled with main memory only when the lock is obtained and when the lock is released. However,
synchronization makes the code less concurrent.
Consider using synchronized if you are updating many
variables and do not want the cost of reconciling each of them with main memory on every access, or you want
to eliminate concurrency for another reason.
Myth 4: Synchronized code is the same as a critical section
Synchronization is often referred to as a critical section. A critical section is one in which only one
thread can execute at a time. It is possible to have synchronized code execute in multiple threads
concurrently.
The confusion occurs because some programmers think that synchronization locks the code
it surrounds. It doesn't. Synchronization locks objects, not code. Therefore, if you have a class with
a synchronized method, this method can be executed concurrently by two different threads if each thread creates its own
object of that class. The code in Listing 3 demonstrates the problem.
class Foo extends Thread
{
private int val;
public Foo(int v)
{
val = v;
}
public synchronized void printVal(int v)
{
while(true)
System.out.println(v);
}
public void run()
{
printVal(val);
}
}
class SyncTest
{
public static void main(String args[])
{
Foo f1 = new Foo(1);
f1.start();
Foo f2 = new Foo(3);
f2.start();
}
}
|
Running the SyncTest code produces interleaved output between 1 and 3. If the printVal method
were a critical section, you would see only 1 or 3 output, not both. Because both are displayed, it proves that
both threads are executing the printVal method concurrently even though it is synchronized and never leaves the
method due to the infinite loop.
To implement a true critical section whereby one, and only one, thread can
ever execute the code at a time, you must synchronize on a global object or on the class literal. The code in
Listing 3 is changed to synchronize on the class literal, thus ensuring that only one thread will ever execute
the synchronized method at a given time. This is shown in Listing 4.
class Foo extends Thread
{
private int val;
public Foo(int v)
{
val = v;
}
public void printVal(int v)
{
synchronized(Foo.class) {
while(true)
System.out.println(v);
}
}
public void run()
{
printVal(val);
}
}
|
Instead of synchronizing on the individual
object instance, the Foo Class is changed to synchronize on the class literal. Because there is only one
class literal for class Foo, both threads are synchronizing on the same lock; therefore, only one thread
will ever execute the printVal method at a time.
This code also could be fixed by synchronizing on a common
object. For example, consider if class Foo had a static object reference. Both methods could synchronize on
this object and achieve thread safety.
Myth 5: Waiting threads are awakened in priority order
When writing multithreaded code, often the situation arises where you have more than one thread waiting
on an event. This occurs when more than one thread calls wait inside a synchronized method or block that is
locked on the same object. Waiting threads are awakened when another thread calls notify or notifyAll from
within a synchronized method or block that is locked on the same object. The notify call wakes up only one
thread. Therefore, if more than one thread is waiting, there will be no contention for the lock. The notifyAll
call, on the other hand, wakes up all waiting threads to compete for the lock. However, only one thread will
get the lock; the others will block.
When multiple threads are waiting, the question is which thread runs
after a call to notify or notifyAll? Many programmers incorrectly assume that there is a predefined order
to how threads are awakened. Some think that the highest priority thread is awakened first, while others
might think it is the thread that has been waiting the longest. Unfortunately, neither assumption is true.
In these situations, the thread that is awakened is undefined. It might be the thread with the highest
priority or the thread that has been waiting the longest, but there is no guarantee that it will be.
The
priority of a thread does not determine whether it is notified (in the case of using the notify method)
or in what order multiple threads are notified (in the case of using the notifyAll method). Therefore,
you should never make assumptions about the order in which threads are awakened as a result of calls to
these methods. In addition, you should never make assumptions about the scheduling of a thread during
preemption. Thread scheduling is implementation-dependent and varies by platform. It is unwise to make
this type of assumption if your code is to be portable.
In addition, the notifyAll method, like the
notify method, does not provide a way to specify the order in which waiting threads are notified.
The order depends on the Java virtual machine,
and no guarantees are made beyond the fact that all waiting threads are awakened. This behavior presents a
problem when you need to notify multiple threads in a particular order.
There are two ways to achieve a
controlled order of awakened threads:
- Use the specific notification pattern
- Use a VM implemented with the Real-Time Specification for Java (RTSJ)
Specific notification pattern
This pattern, developed by Tom Cargill,
specifies how you can control the order of threads to be awakened from a call to notify and notifyAll. This is
accomplished through a separate lock object for each thread, or set of threads, that needs to be notified
together. This enables notification to happen in a defined order to a specific lock object and thus to its
associated thread or threads.
The execution overhead of this pattern is minimal if implemented properly. There
is, however, some added code complexity. This added complexity can be displaced with the additional control
this pattern gives you. You should consider implementing this pattern if you have the need to control the
notification order of multiple threads.
RTSJ
The RTSJ changes the standard behavior of certain Java
semantics. One of these semantics is ensuring that waiting threads are queued in priority order. Therefore,
when one or more threads are waiting and a call is made to notify or notifyAll, the thread with the highest
priority executes first. The others must wait. Generally, it is not recommended to use a real-time
implementation for anything but real-time programs. There are various trade-offs that are made to enable
the Java programming language for real-time programming. One overriding principle in the creation of the
RTSJ is that timeliness takes precedence over execution speed.
Running the samples with VisualAge for Java
The following steps show how the sample code can be run with the VisualAge for Java, Professional
Edition, Version 4.0 product:
- Copy the code into a directory of your choice.
- Open the workbench into an existing workspace.
- Add a project called JavaMyths.
- From the JavaMyths project, import the directory
where you stored the code from this article.
- Open the JavaMyths project to view, update, compile, and run the code.
Conclusion
Conventional wisdom is not always accurate. Code written with assumptions about any of these myths can result in inefficient code or code that is simply broken. In addition, some of the myths only apply to certain virtual machine implementations. For example, coding without knowledge of Myth 3 can result in code that behaves properly on one or more VM implementations, but incorrectly on others. This type of problem is extremely difficult to debug. The same type of situation can exist when coding to Myth 5. On several executions of your code, the highest priority thread might be the one that is awakened after a call to notify or notifyAll. However, this might not be the behavior on every execution of the code or of the same code run on different VM implementations.
Resources - Categories: Java tools, VisualAge
- VisualAge for Java
- IBM Developer Kit for Windows
About the author  | 
|  |
Peter Haggar is a Senior Software Engineer with IBM in Research Triangle Park, North
Carolina, and the author of the best-selling book Practical Java Programming Language Guide published by Addison-Wesley. He has a broad range of programming experience, having worked on development tools, class
libraries, and operating systems. At IBM, Peter works on emerging Java technology and is currently the
specification lead for the Real-Time Specification for Java. Peter is a frequent technical speaker on
Java technology at numerous industry conferences. He has worked for IBM for more than 14 years and
received a B.S. in Computer Science from Clarkson University. You can contact him at haggar@us.ibm.com.
|
Rate this page
|