Java中生成一个线程安全的计数器

上一篇讲了在Java中同步访问共享的可变数据有两种方法,一个是使用sychronized关键字来同步方法或同步块,这种方法能同步数据并且还能实现访问互斥。另外一种方法是使用volatile修饰符,它仅仅实现了线程之间的交互通信。

假设我们现在需要实现一个简单的计数器,它是一个不重复且唯一的递增数值。我们使用volatile来实现看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Counter {
private static volatile int num = 0;//初始值
private static final int LIMIT = 50;//自增限制

private static int generateNum() {
return ++num;
}

private static void incrNum() throws InterruptedException {
while (num < LIMIT) {
Thread.sleep(100);
if (num < LIMIT) {System.out.println(Thread.currentThread().getName()+": "+generateNum());
}
}

}

public static void main(String[] args) throws InterruptedException {
//开5个线程跑
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
incrNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Thread-0: 2
Thread-4: 5
Thread-3: 4
Thread-2: 3
Thread-1: 1
Thread-0: 6
Thread-1: 8
Thread-2: 9
Thread-4: 6
Thread-3: 7
Thread-0: 10
Thread-2: 12
Thread-4: 13
Thread-1: 11
Thread-3: 14
Thread-4: 15
Thread-3: 17
Thread-1: 16
Thread-0: 15
Thread-2: 15
…………
Thread-4: 43
Thread-3: 44
Thread-0: 44
Thread-2: 45
Thread-1: 46
Thread-4: 47
Thread-0: 49
Thread-3: 48

可以看到上述结果有很多值都是重复的,这显然不是我们想要的效果。我们的方法是确保每个调用都返回不同的值。

问题在于,增量操作符++不是原子的。它在num域中执行两项操作:首先它读取值,然后写回一个新值,相当于原来的值再加上1。如果这时候第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会和第一个线程一起看到同一个值,并返回相同的新值。这就是安全性失败

要修正incrNum方法的一种方法是在它的声明中添加synchronized修饰符来实现互斥。这样可以确保多个调用不会交叉存取,每个调用都会看到之前所有调用的效果,然后num的修饰符volatile就可以删除了,因为synchronized已经达到了同步的效果。像下面这样:

1
2
3
4
private static int num = 0;
public synchronized static int generateNum() {
return ++num;
}

其它地方不需要改动,这样就可以达到想要的效果了。为了让这个方法更可靠,最好用long代替int

还有第二种方法也可以实现相同的效果:使用类AtomicLong,它是java.util.concurrent.atomic的一部分,它比synchronized修饰符执行得更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Counter {
private static AtomicLong serialNumber = new AtomicLong(0);
private static final int LIMIT = 50;

private static long generateSerialNumber() {
return serialNumber.incrementAndGet();
}

private static void incrSerialNumber() throws InterruptedException {
while (serialNumber.get() < LIMIT) {
Thread.sleep(100);
if (serialNumber.get() < LIMIT) {
System.out.println(Thread.currentThread().getName()+": "+generateSerialNumber());
}
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
incrSerialNumber();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Thread-0: 1
Thread-1: 2
Thread-2: 3
Thread-4: 4
Thread-3: 5
Thread-0: 6
Thread-1: 7
Thread-2: 8
Thread-4: 9
Thread-3: 10
…………
Thread-3: 40
Thread-0: 41
Thread-3: 43
Thread-4: 42
Thread-2: 45
Thread-1: 44
Thread-0: 46
Thread-3: 47
Thread-4: 48
Thread-2: 49
Thread-1: 50

那么AtomicLong这个类的原理是什么呢?看这一篇理解Java中的AtomicLong类