synchronized有趣的总结

Java中synchronized关键词是一个有趣的东东,它到底锁的是什么呢?

synchronized锁的到底是什么

下面将用多个实例说明synchronized锁的到底是什么。实例将演示100个线程分别累加20,如果计算正确,将输出结果2000.

  • 锁的是代码?

我们这段代码将synchronized放在方法上,经多次运行后,偶尔也会出现小于2000的值,说明synchronized没起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SyncFunc implements Runnable {
public static int count = 0;

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new SyncFunc()).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}

@Override
public synchronized void run() {
for (int i = 0; i < 100; i++) {
count++;
}
}
}

输出结果:

1
1970、2000、2000

  • 真的不是锁代码?

我们将synchronized换了一个位置,这回锁住的是this,按理说和上面的代码没有区别,但我们这回把this对象一并输出,让我们看看执行结果。

由结果可见,synchronized锁住相同的代码,依然会出现并发问题。但我们发现synchronized锁住的对象内存地址不同,那么synchronized锁的是对象吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SyncFunc implements Runnable {
public static int count = 0;

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new SyncFunc()).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}

@Override
public void run() {
System.out.println(this);
synchronized (this) {
for (int i = 0; i < 100; i++) {
count++;
}
}
}
}

部分输出结果:

1
2
3
4
5
6
7
synchronize.SyncFunc@52bd51dc
synchronize.SyncFunc@6b2ce86d
synchronize.SyncFunc@7f47ad69
synchronize.SyncFunc@b9098ae
synchronize.SyncFunc@719e4b38
synchronize.SyncFunc@586f4a16
1999

  • 锁的是对象吗?

将上面的代码稍做修改,将Thread的target更改为同一个对象,这回运行多次也不会出现并发问题了,而且输出this的值也是同一个对象的地址。

那么,得出结论synchronized锁的是对象,而不是代码。也就是说,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
public class SyncFunc implements Runnable {
public static int count = 0;

public static void main(String[] args) throws InterruptedException {
SyncFunc syncFunc = new SyncFunc();
for (int i = 0; i < 20; i++) {
new Thread(syncFunc).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}

@Override
public void run() {
System.out.println(this);
synchronized (this) {
for (int i = 0; i < 100; i++) {
count++;
}
}
}
}

部分输出结果:

1
2
3
4
5
6
7
synchronize.SyncFunc@3ffc8195
synchronize.SyncFunc@3ffc8195
synchronize.SyncFunc@3ffc8195
synchronize.SyncFunc@3ffc8195
synchronize.SyncFunc@3ffc8195
synchronize.SyncFunc@3ffc8195
2000

synchronized易混淆问题

  • 对Integer类型变量的锁

我们再将上面正确的代码修改一下,将临界区域变小,因为我们只对变量count的值做修改,那么只需要锁住count变量就行了吗?

答案是否定的,因为运行结果已经说明了一切,问题就在于count++操作。首先因为synchronized只能对对象进行锁定,所以count不能使用基本数据类型int。Integer类型的count变量,在进行自增操作时,通过拆箱-自增-装箱几步操作过后,count变量已经不是同一个对象了,这意味着每个线程锁住的count变量都不尽相同。

p.s.String类型也容易出现该问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SyncFunc implements Runnable {
public static Integer count = 0;

public static void main(String[] args) throws InterruptedException {
SyncFunc syncFunc = new SyncFunc();
for (int i = 0; i < 20; i++) {
new Thread(syncFunc).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}

@Override
public void run() {
synchronized (count) {
for (int i = 0; i < 100; i++) {
count++;
}
}
}
}

输出结果:

1
1856

  • class锁

上面说过,synchronized只能对同一个对象进行加锁,那我偏偏要锁住不同的对象呢?可以直接使用synchronized锁住该对象对应的类,这样对所有线程所有对象都有效,但弊端就是临界区域太大,影响运行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SyncFunc implements Runnable {
public static int count = 0;

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new SyncFunc()).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}

@Override
public void run() {
synchronized (SyncFunc.class) {
for (int i = 0; i < 100; i++) {
count++;
}
}
}
}

输出结果:

1
2000

最终总结

  • synchronized锁住的是不同线程对同一个对象的访问,而不是锁住的代码。
  • synchronized锁住的对象是IntegerString等类型时需要密切注意,因为他们锁住的对象地址可能发生变化。
  • synchronized可以使用synchronized(Main.class)的形式锁住该类,对所有线程所有实例都能起作用。