0%

Java 并发基础知识(二):理解 synchronized

1. synchronized 的用法

  • synchronized 可以用于修饰类的实例方法静态方法、和代码块

2. synchronized 修饰实例方法的基本原理

  • 加了 synchronized 修饰符后,方法内的代码就变成了原子操作
  • synchronized 实例方法实际保护的是同一个对象的方法调用,确保同时只能有一个线程执行
  • synchronized 实例方法保护的是当前实例对象,即 thisthis 对象有一个和一个等待队列锁只能被一个线程持有,其他试图获得同样锁的线程需要等待

3. 执行 synchronized 实例方法的过程大致是

  • synchronized 的实际执行过程很复杂,而且 Java 虚拟机采用了多种优化方式以提高性能
  • 从概念上,可以如下简单理解
    • 尝试获得锁,如果能够获得锁,继续下一步,否则加入等待队列,阻塞并等待唤醒
    • 执行实例方法体代码
    • 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性
    • 当前线程不能获得锁的时候,它会加入等待队列等待,线程的状态会变为 BLOCKED

4. synchronized 修饰实例方法的注意事项

  • synchronized 保护的是对象而非代码,只要访问的是同一个对象的 synchronized 方法,即使是不同的代码,也会被同步顺序访问
  • synchronized 方法不能防止非 synchronized 方法被同时执行
  • 一般在保护变量时,需要在所有访问该变量的方法上加上 synchronized

5. synchronized 修饰静态方法的基本原理

  • synchronized 保护的是对象。对静态方法,synchronized 保护的是类对象
  • 每个对象都有一个和一个等待队列,类对象也不例外
  • synchronized 静态方法和 synchronized 实例方法保护的是不同的对象。不同的两个线程,可以一个执行 synchronized 静态方法,另一个执行 synchronized 实例方法

6. synchronized 包装代码块的基本原理

  • synchronized 括号里面的就是要保护的对象
  • synchronized 同步的对象可以是任意对象,任意对象都有一个锁和等待队列。或者说,任何对象都可以作为锁对象

7. 请用代码块的方式写出下面代码的等价形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//synchronized 修饰实例方法
public class Counter {
private int count;
public synchronized void incr() {
count++;
}
public synchronized int getCount() {
return count;
}
}

//synchronized 修饰静态方法
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//synchronized 代码块修饰的 Counter 类
public class Counter {
private int count;
public void incr() {
synchronized(this) {
count++;
}
}
}

//synchronized 代码块修饰的 StaticCounter 类
public class StaticCounter {
private static int count = 0;
public static void incr() {
synchronized(StaticCounter.class) {
count++;
}
}
public static int getCount() {
synchronized(StaticCounter.class) {
return count;
}
}
}

8. 使用单独对象作为锁的 Counter 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Counter {
private int count;
private Object lock = new Object();
public void incr() {
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}

9. synchronized 可重入性的概念

  • 概念:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用
  • 举例:在一个 synchronized 实例方法内,可以直接调用其他 synchronized 实例方法
  • 范围:可重入是一个非常自然的属性,很容易理解,但并不是所有的锁都是可重入的

10. synchronized 的可重入性是怎样实现的

  • 原理:可重入是通过记录锁的持有线程持有数量来实现的
  • 细节:当调用被 synchronized 保护的代码时,检查对象是否已被锁。如果是,再检查是否被当前线程锁定。如果是,增加持有数量;如果不是被当前线程锁定,才加入等待队列。当释放锁时,减少持有数量,当数量变为 0 时才释放整个锁

11. 怎样理解 synchronized 保证内存可见性

  • synchronized 除了保证原子操作外,还有一个重要的作用,就是保证内存可见性
  • 释放锁时,所有写入都会写回内存
  • 获得锁后,都会从内存中读最新数据

12. 关键字 volatile 的作用及原理

  • 作用:给变量volatile 修饰符是更轻量级的保证内存可见性的方式。如果只是为了保证内存可见性,使用 synchronized 的成本有点高
  • 原理:加了 volatile 之后,Java 会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存的值
  • 结论volatile 可以保证内存可见性有序性(禁止指令重排),但不能保证原子性
  • 参考:关于 volatile 的两篇不错的文章

13. 什么叫死锁

  • 死锁就是类似这种现象,比如,有 A、B 两个线程,A 持有锁 a,在等待锁 b;而 B 持有锁 b,在等待锁 a
  • 线程 A 和 B 陷入了互相等待,最后谁都执行不下去

14. 手写一个死锁 Demo

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
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();

private static void startThreadA() {
Thread aThread = new Thread() {
@Override
public void run() {
synchronized(lockA) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {

}

synchronized(lockB) {
//do something
}

}
}
};
aThread.start();
}

private static void startThreadB() {
Thread bThread = new Thread() {
@Override
public void run() {
synchronized(lockB) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {

}

synchronized(lockA) {
//do something
}
}
}
};
bThread.start();
}

public static void main(String[] args) { //运行后 aThread 和 bThread 陷入了互相等待,最后谁都执行不下去,导致死锁
startThreadA();
startThreadB();
}
}

15. 怎样解决死锁

  • 首先,应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的约定顺序去申请锁

  • 不过,在复杂的项目中,这种约定可能难以做到

    • 此时,可以使用显示锁接口 Lock,它支持尝试获取锁(tryLock)带时间限制的获取锁方法
    • 使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁干脆放弃,以避免死锁
  • 如果还是出现了死锁,Java 不会主动处理。此时,可以借助一些工具发现运行中的死锁,比如,Java 自带的 jstack 命令会报告发现的死锁

16. Java 中的同步容器内部是怎样实现的

  • Collection 中有一些方法,可以返回线程安全的同步容器
  • 它们是给所有容器方法都加上 synchronized 来实现安全的

17. 加了 synchronized,所有方法调用变成了原子操作,客户端在调用时,是不是就绝对安全了呢

  • 不会绝对安全,至少有以下情况需要注意
    • 复合操作(比如先检查再更新)
    • 伪同步(同步错对象)
    • 迭代(遍历时给整个容器对象加锁)

18. 简要描述一下 Java 中的并发容器

  • 同步容器的性能是比较低的,当并发访问量比较大的时候性能比较差

  • Java 中有很多专为并发设计的容器类,比如

    • CopyOnWriteArrayList
    • ConcurrentHashMap
    • ConcurrentLinkedQueue
    • ConcurrentSkipListSet
  • 这些容器类都是线程安全的,但都没有使用 synchronized,没有迭代问题,直接支持一些复合操作,性能也高得多

19. JVM 内存结构、Java 内存模型、Java 对象模型的区别

-------------------- 本文结束感谢您的阅读 --------------------