Threading
Modern computers are capable of threading -- i.e. performing several tasks at the same time
using the same memory.
In this first example we create two threads, each of which will update the static
variable i 10,000 times.
Creating threads is easy. We subclass java.lang.Thread, then supply a run() method.
We call start() to get the thread running, and we call join() to wait for it to
stop. Easy.
Thread1.java |
1 | public class Thread1 extends Thread { |
2 | static int i = 0; |
3 | public void run() { |
4 | for(int j=0;j<10000000;j++) |
5 | i++; |
6 | } |
7 | public static void main(String[] args) throws InterruptedException { |
8 | long time1 = System.nanoTime(); |
9 | Thread t1 = new Thread1(); |
10 | Thread t2 = new Thread1(); |
11 | t1.start(); |
12 | t2.start(); |
13 | t1.join(); |
14 | t2.join(); |
15 | System.out.println("i="+i); |
16 | long time2 = System.nanoTime(); |
17 | System.out.println("time = "+(time2-time1)); |
18 | } |
19 | } |
$ javac Thread1.java
| $ java Thread1
i=10021830
time = 21107837
|
What went wrong? The operation "i++" is actually performed in steps, first a read,
then an increment, then a write back to main memory. It may happen that thread 2
reads a value directly after thread 1 does, before thread 1 has a chance to do
an increment. In this case one of the increments are lost.
This is called a race condition, and it can be very difficult to debug.
Let's try calling the run() methods directly, without creating a thread.
Thread1a.java |
1 | public class Thread1a extends Thread { |
2 | public static void main(String[] args) throws InterruptedException { |
3 | long time1 = System.nanoTime(); |
4 | Thread t1 = new Thread1(); |
5 | Thread t2 = new Thread1(); |
6 | t1.run(); |
7 | t2.run(); |
8 | System.out.println("i="+Thread1.i); |
9 | long time2 = System.nanoTime(); |
10 | System.out.println("time = "+(time2-time1)); |
11 | } |
12 | } |
$ javac Thread1a.java
| $ java Thread1a
i=20000000
time = 17784142
|
Because we never called start, we never really ran any new threads. This solved
our race condition, of course, because we only ever had one thread manipulating
the data.
Ok, that was a lot slower. We want to go back to using start()/join(), but without
the race condition occurring.
There are several ways we can go about fixing this.
Thread2.java |
1 | public class Thread2 extends Thread { |
2 | static int i = 0; |
3 | static synchronized void inc() { |
4 | i++; |
5 | } |
6 | public void run() { |
7 | for(int j=0;j<10000000;j++) |
8 | inc(); |
9 | } |
10 | public static void main(String[] args) throws InterruptedException { |
11 | long time1 = System.nanoTime(); |
12 | Thread t1 = new Thread2(); |
13 | Thread t2 = new Thread2(); |
14 | t1.start(); |
15 | t2.start(); |
16 | t1.join(); |
17 | t2.join(); |
18 | System.out.println("i="+i); |
19 | long time2 = System.nanoTime(); |
20 | System.out.println("time = "+(time2-time1)); |
21 | } |
22 | } |
$ javac Thread2.java
| $ java Thread2
i=20000000
time = 966481550
|
The notion here is that we were able to synchronize our access to the variable "i".
Synchronization is a very helpful tool, and it can prevent race conditions -- but it
has its own dangers.
Thread3.java |
1 | public class Thread3 extends Thread { |
2 | static Thread3 a, b; |
3 | |
4 | int i = 0; |
5 | |
6 | public void run() { |
7 | for(int i=0;i<10000000;i++) |
8 | if(i % 2 == 0) |
9 | inc_a_then_b(); |
10 | else |
11 | inc_b_then_a(); |
12 | } |
13 | |
14 | void inc_a_then_b() { |
15 | synchronized(a) { |
16 | a.i++; |
17 | synchronized(b) { |
18 | b.i++; |
19 | } |
20 | } |
21 | } |
22 | |
23 | void inc_b_then_a() { |
24 | synchronized(b) { |
25 | b.i++; |
26 | synchronized(a) { |
27 | a.i++; |
28 | } |
29 | } |
30 | } |
31 | |
32 | public static void main(String[] args) throws InterruptedException { |
33 | long time1 = System.nanoTime(); |
34 | a = new Thread3(); |
35 | b = new Thread3(); |
36 | a.start(); |
37 | b.start(); |
38 | a.join(); |
39 | b.join(); |
40 | System.out.println("i="+a.i); |
41 | long time2 = System.nanoTime(); |
42 | System.out.println("time = "+(time2-time1)); |
43 | } |
44 | } |
$ javac Thread3.java
|
I can't show you what this code does. You'll have to try it for yourself.
It may take more than one attempt, but eventually you'll see it. The code just
stops and you get no output.
What went wrong? Suppose thread a has synchronized on itself
and thread b has synchronized on itself, and then each waits for the lock
on the other. They will wait forever. This is called deadlock.
Threading looks deceptively simple, but almost any task that you implement
using thread and synchronized is fraught with peril.
|