We have seen two ways to spawn a thread, using the Thread
and AsyncTask
classes. When two or more threads access the same data, you need to make sure the data types support concurrent access. The Java language defines many classes in the java.util.concurrent
package for that purpose:
You will have to carefully select your data types based on your application's requirements. Also, the fact that they are concurrent implementations does not necessarily imply that operations are atomic. In fact, many operations are not atomic and by design are not meant to be. For example, the putAll()
method in the ConcurrentSkipListMap
class is not atomic. A concurrent implementation merely means that the data structure will not be corrupted when accessed by multiple threads.
If you want to share objects between multiple threads but have not implemented any fine-grained locking mechanism, you can use the synchronized
keyword to make sure your access is thread-safe, as shown in Listing 5–10.
public class MyClass {
private int mValue;
public MyClass(int n) {
mValue = n;
}
public synchronized void add (int a) {
mValue += a;
}
public synchronized void multiplyAndAdd (int m, int a) {
mValue = mValue * m + a;
}
}
The two methods add
and multiplyAndAdd
in Listing 5–7 are synchronized methods. This means two things:
The first item is quite intuitive. The second one should be as well although it still requires an explanation. As a matter of fact, the Java memory model is such that a modification to a variable in one thread may not immediately be visible to other threads. Actually, it may never be visible. Consider the code in Listing 5–11: if one thread calls MyClass.loop()
, and at some point in the future another thread calls Myclass.setValue(100)
, the first thread may still not terminate; may carry on looping forever and always print out a value other than 100, simply because of the Java language's memory model.
public class MyClass {
private static final String TAG = "MyClass";
private static int mValue = 0;
public static void setValue(int n) {
mValue = n;
}
public static void loop () {
while (mValue != 100) {
try {
Log.i(TAG, “Value is ” + mValue);
Thread.sleep(1000);
} catch (Exception e) {
// ignored
}
}
}
}
You have two options to fix that:
synchronized
keyword, as shown in Listing 5–12.volatile
keyword, as shown in Listing 5–13.public class MyClass {
private static final String TAG = "MyClass";
private static int mValue = 0;
public static synchronized void setValue(int n) {
mValue = n;
}
public static synchronized int getValue() {
return mValue;
}
public static void loop () {
int value;
while ((value = getValue()) != 100) {
try {
Log.i(TAG, “Value is ” + value);
Thread.sleep(1000);
} catch (Exception e) {
// ignored
}
}
}
}
public class MyClass {
private static final String TAG = "MyClass";
private static volatile int mValue = 0; // we add the volatile keyword and remove
the synchronize keyword
public static void setValue(int n) {
mValue = n; // you'd still have to use synchronized if that statement were
mValue += n (not atomic)
}
public static void loop () {
while (mValue != 100) {
try {
Log.i(TAG, “Value is ” + mValue);
Thread.sleep(1000);
} catch (Exception e) {
// ignored
}
}
}
}
NOTE: Make sure you understand which statements are atomic. For example, value++
is not atomic while value = 1
is. This is important as the volatile
keyword can only fix concurrency issues if the statements using the variable are atomic. If they are not, then you have to use the synchronized
keyword instead.
You can improve concurrency and throughput by using synchronized statements, as shown in Listing 5–14, as opposed to making whole methods synchronized. In these cases, you want to protect only the part that needs to be protected (that is, where mValue
is being modified), but leave the log message outside of the synchronized block. You can also use objects other than this
as a lock.
public class MyOtherClass {
private static final String TAG = "MyOtherClass";
private int mValue;
private Object myLock = new Object(); // more than this… there is
public MyClass(int n) {
mValue = n;
}
public void add (int a) {
synchronized (myLock) {
mValue += a;
}
Log.i(TAG, "add: a=" + a); // no need to block
}
public void multiplyAndAdd (int m, int a) {
synchronized (myLock) {
mValue = mValue * m + a;
}
Log.i(TAG, " multiplyAndAdd: m=" + m + ", a=" + a); // no need to block
}
}
Making methods or statements synchronized is the easiest way to guarantee your class supports concurrent access. However, it may reduce the throughput when not everything needs to be protected, and even worse, it can cause deadlocks. Indeed, deadlocks can occur when you call another object's method from within a synchronized block, which may attempt to acquire a lock on an object that is already locked and waiting for your own object's lock.
TIP: Don't call another object's method within a synchronized block unless you can guarantee no deadlock will occur. Usually, you can guarantee that only when you are the author of the code of the other object's class.
In general, it is best to avoid accessing the same object from different threads if you have any doubt about whether it will work or not. The classes defined by Java are a good reference, and since the Android code is available, you can refer to the implementation of these classes to understand the various techniques you can take advantage of. In addition, to simplify your development, Java defines many classes that already support thread-safe or concurrent programming, which can be used as the basis for some of your own algorithms.
18.219.191.233