通过代码学习 - Java中的Synchronized

Synchronized 关键字

无Synchronized

Synchronized是互斥锁,非常接近底层Mux的概念,就是把内存的一块区间给锁住,只允许一个线程使用。

1
2
3
4
5
6
7
8
class Sender {
private int count = 0;

public void printIncrease() {
count = count + 1; //这里注意不能为 count++;这是一个原子性操作
System.out.println(Thread.currentThread().toString() + ":" + "count is " + count);
}
}

我们首先创建一个Sender类,我们声明一个printIncrease函数,注意此时没有 synchronized 关键字。我们再构建一个测试Demo,模拟50个线程同时运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Synchronization {

public static void main(String[] args) {
Sender s = new Sender();
List<Thread> runnables = new ArrayList<>();


for(int i = 0; i<50;i++){
runnables.add(new Thread(s::printIncrease));
}

runnables.forEach(Thread::start);
runnables.forEach((r) -> {
try {
r.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

}
}

理论上我们最后一次的打印出来的Counter应该是49,因为我们运行了50次。但是在本机的结果是如下图(根据机器都不太一样)

1
2
3
4
5
6
Thread[Thread-44,5,main]:count is 44
Thread[Thread-45,5,main]:count is 44
Thread[Thread-46,5,main]:count is 46
Thread[Thread-48,5,main]:count is 47
Thread[Thread-49,5,main]:count is 47
Thread[Thread-47,5,main]:count is 47

Synchronized方式

那我们此时加上 synchronized

1
2
3
4
5
6
7
8
class Sender {
private int count = 0;

public synchronized void printIncrease() {
count = count + 1;
System.out.println(Thread.currentThread().toString() + ":" + "count is " + count);
}
}

结果如图如下:

1
2
3
Thread[Thread-47,5,main]:count is 47
Thread[Thread-48,5,main]:count is 49
Thread[Thread-49,5,main]:count is 50

我们可以看出来synchronized第一特性就是同步函数的特性。

Synchronized 属性

我们换一种方式,我们synchronized 这个 count 属性,结果呢?

1
2
3
4
5
6
7
8
9
10
class Sender {
private Integer count = 0;

public void printIncrease() {
synchronized (count) {
count = count + 1;
}
System.out.println(Thread.currentThread().toString() + ":" + "count is " + count);
}
}
1
2
3
4
5
6
Thread[Thread-1,5,main]:count is 0
Thread[Thread-4,5,main]:count is 0
Thread[Thread-3,5,main]:count is 0
Thread[Thread-2,5,main]:count is 0
Thread[Thread-0,5,main]:count is 0
Thread[Thread-6,5,main]:count is 4

我们发现没有完成我的需求,这是为什么呢,这是因为count并非是一个final的对象,其实每一个锁都是加在不同的对象上的。那我们应该怎么写呢?

1
2
3
4
5
6
7
8
9
10
11
class Sender {
private final Object lock = new Object();
private Integer count = 0;

public void printIncrease() {
synchronized (lock) {
count = count + 1;
}
System.out.println(Thread.currentThread().toString() + ":" + "count is " + count);
}
}

我们在内部申明一个变量就可以达到这样的效果。但是结果是怎么样的呢?

1
2
3
Thread[pool-1-thread-2,5,main]:synchronization.Sender@65570b43count is 2
Thread[pool-1-thread-1,5,main]:synchronization.Sender@65570b43count is 2
Thread[pool-1-thread-3,5,main]:synchronization.Sender@65570b43count is 3

我们依然发现有并发的问题,很多同学会觉得是 DCL双检锁 的问题,实际上并非是,这里的问题在于
System.out.println 的代码是

1
2
3
4
5
6
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

我们发现这里其实有自己的一个Lock,相当在过程中会出现线程之间修改了Counter的值,但是我们可以看出来最后值是对的,说明我们执行的次数是正确的,如果避免这个问题呢?

方法一

1
2
3
4
5
6
public void printIncrease() {
synchronized (lock) {
count = count + 1;
System.out.println(Thread.currentThread().toString() + ":" + this.toString() + "count is " + count);
}
}

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sender {
private final Object lock = new Object();
private volatile int count = 0;

public void printIncrease() {
int realCount;
synchronized (lock) {
count = count + 1;
realCount = count;
}

System.out.println(Thread.currentThread().toString() + ":" + this.toString() + "count is " + realCount);
}
}

Synchronized 类型

我们再尝试将 此类型的 Class 锁住,效果也可以达成,因为Class其实也在我们内存中的一块区域。

1
2
3
4
5
6
7
8
9
10
class Sender {
private Integer count = 0;

public void printIncrease() {
synchronized (Sender.class) {
count = count + 1;
}
System.out.println(Thread.currentThread().toString() + ":" + "count is " + count);
}
}
1
2
3
Thread[Thread-47,5,main]:count is 48
Thread[Thread-48,5,main]:count is 49
Thread[Thread-49,5,main]:count is 50

Synchronized 多实例

OK,这个时候如果申明多个实例呢?尝试一下。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Synchronization {

public static void main(String[] args) throws InterruptedException {
Sender s = new Sender();
Sender s2 = new Sender();
LinkedList<SimpleCallback> runnables = new LinkedList<>();

for (int i = 0; i < 50; i++) {
if (i % 2 == 0) {
runnables.add(i, new SimpleCallback(s));
} else {
runnables.add(i, new SimpleCallback(s2));
}
}

ExecutorService executorService = Executors.newFixedThreadPool(50);
executorService.invokeAll(runnables);
executorService.awaitTermination(5, TimeUnit.SECONDS);
executorService.shutdown();
}
}


class Sender {
private final Object lock = new Object();
private volatile int count = 0;

public void printIncrease() {
int realCount;
synchronized (lock) {
count = count + 1;
realCount = count;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println(Thread.currentThread().toString() + ":" + this.toString() + "count is " + realCount);
}
}

class SimpleCallback implements Callable<Void> {

private final Sender sender;

SimpleCallback(Sender sender) {
this.sender = sender;
}

@Override
public Void call() throws Exception {
sender.printIncrease();
return null;
}
}


1
2
3
4
5
Thread[pool-1-thread-2,5,main]:synchronization.Sender@af58fe3count is 1
Thread[pool-1-thread-1,5,main]:synchronization.Sender@17748af7count is 1
1s 之后
Thread[pool-1-thread-50,5,main]:synchronization.Sender@af58fe3count is 2
Thread[pool-1-thread-49,5,main]:synchronization.Sender@17748af7count is 2

我们可以发现其实锁的本质就在于增加某一个变量的内存空间,如果是不同的对象自然是不同的锁。大胆的猜测如果锁加在类上,因为是唯一的地址空间那因为是一个接着一个启动的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Sender {
private final Object lock = new Object();
private volatile int count = 0;

public void printIncrease() {
int realCount;
synchronized (Sender.class) {
count = count + 1;
realCount = count;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println(Thread.currentThread().toString() + ":" + this.toString() + "count is " + realCount);
}
}

我们可以看见结果

1
2
3
4
5
6
Thread[pool-1-thread-1,5,main]:synchronization.Sender@4878c7d7count is 1
1s 之后
Thread[pool-1-thread-50,5,main]:synchronization.Sender@7b267845count is 1
1s 之后
Thread[pool-1-thread-49,5,main]:synchronization.Sender@4878c7d7count is 2
1s 之后

Synchronized 可重入性

这个特性主要是针对当前线程而言的,可重入即是自己可以再次获得自己的内部锁,在尝试获取对象锁时,如果当前线程已经拥有了此对象的锁,则把锁的计数器加一,在释放锁时则对应地减一,当锁计数器为0时表示锁完全被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DeadLock {

public synchronized void method1() {
}

public synchronized void method2() {
this.method1();
}

public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
deadLock.method2();
}
}

Synchronized 的非公平

1
TODO 如何复现

其他

synchronized最后一个特性(缺点)就是不可中断性,在所有等待的线程中,你们唯一能做的就是等,而实际情况可能是有些任务等了足够久了,我要取消此任务去干别的事情,此时synchronized是无法帮你实现的,它把所有实现机制都交给了JVM,提供了方便的同时也体现出了自己的局限性。