1. synchronized
的用法
synchronized
可以用于修饰类的实例方法、静态方法、和代码块
2. synchronized
修饰实例方法的基本原理
- 加了
synchronized
修饰符后,方法内的代码就变成了原子操作 synchronized
实例方法实际保护的是同一个对象的方法调用,确保同时只能有一个线程执行synchronized
实例方法保护的是当前实例对象,即this
。this
对象有一个锁和一个等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待
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 | //synchronized 修饰实例方法 |
1 | //synchronized 代码块修饰的 Counter 类 |
8. 使用单独对象作为锁的 Counter 类
1 | public class Counter { |
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 | public class DeadLockDemo { |
15. 怎样解决死锁
首先,应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的约定顺序去申请锁
不过,在复杂的项目中,这种约定可能难以做到
- 此时,可以使用显示锁接口
Lock
,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法 - 使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁
- 此时,可以使用显示锁接口
如果还是出现了死锁,Java 不会主动处理。此时,可以借助一些工具发现运行中的死锁,比如,Java 自带的 jstack 命令会报告发现的死锁
16. Java 中的同步容器内部是怎样实现的
- 类
Collection
中有一些方法,可以返回线程安全的同步容器 - 它们是给所有容器方法都加上
synchronized
来实现安全的
17. 加了 synchronized
,所有方法调用变成了原子操作,客户端在调用时,是不是就绝对安全了呢
- 不会绝对安全,至少有以下情况需要注意
- 复合操作(比如先检查再更新)
- 伪同步(同步错对象)
- 迭代(遍历时给整个容器对象加锁)
18. 简要描述一下 Java 中的并发容器
同步容器的性能是比较低的,当并发访问量比较大的时候性能比较差
Java 中有很多专为并发设计的容器类,比如
CopyOnWriteArrayList
ConcurrentHashMap
ConcurrentLinkedQueue
ConcurrentSkipListSet
这些容器类都是线程安全的,但都没有使用
synchronized
,没有迭代问题,直接支持一些复合操作,性能也高得多