刚才面试的时候被问到了关于线程安全和死锁的问题,有点露怯,故赶紧查漏补缺,记录于此。
线程安全
线程安全是程序设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。
乐观锁与悲观锁
- 乐观锁:认为在使用数据时,不会有别的线程修改数据,所以不会加锁,只在更新时判断之前有没有被别的线程更新了数据。比如在数据库中设置一个
version
字段,在更新前先查询该字段的值,然后在写入时比较数据库中的值是否与之前查询到的值相同。 - 悲观锁:认为自己在使用数据的时候,一定有别的线程来修改数据,因此在获取数据的时候先加锁,确保数据不会被线程修改。
如何保证线程安全
syncronized
关键字,举例:ConcurrentHashMap
。是悲观锁。- 锁升级机制:
它是指在锁对象的对象头里面有一个
threadid
字段,在第一次访问的时候threadid
为空,JVM 让其持有偏向锁,并将threadid
设置为其线程 ID,再次进入的时候会先判断threadid
是否与其线程 ID 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized
锁的升级。- 偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的 id 会记录在对象的
Mark Word
中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。 - 轻量级锁(CAS):就是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过 CAS 操作尝试将
Mark Word
更新为指向LockRecord
的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。 - 重量级锁:虚拟机使用 CAS 操作尝试将
MarkWord
更新为指向LockRecord
的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord
是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
- 偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的 id 会记录在对象的
- 锁升级机制:
Lock
接口的实现类,常用ReentrantLock
。是悲观锁。lock()
加锁,unlock()
解锁,不解锁会造成死锁。- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。 - 锁绑定多个条件:一个
ReentrantLock
对象可以同时绑定多个Condition
对象,而在synchronized
中,锁对象的wait()
和notify()
或notifyAll()
方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock
则无须这样做,只需要多次调用newCondition()
方法即可。
ThreadLocal
。当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal
来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。ThreadLocal
线程容器保存变量时,底层其实是通过ThreadLocalMap
来实现的。它是以当前ThreadLocal
变量为 key,要存的变量为 value。获取的时候就是以当前ThreadLocal
变量去找到对应的 key,然后获取到对应的值。
死锁
两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作 “资源” 的东西。
检测死锁
可以使用 jstack
检查死锁。
命令:jstack $(jps -l | grep 'DeadLockExample' | cut -f1 -d ' ')
。
示例输出:
1 | Java stack information for the threads listed above: |
避免死锁
- 以确定的顺序获锁
- 超时放弃
- 死锁检测
- 尽量降低锁的使用粒度
- 尽量使用同步代码块,而不是同步方法
- 避免嵌套锁
- 专锁专用
参考文章
- 4 种解决线程安全问题的方式
- Java 高级教程系列 - 死锁示例及解决
- Java 多线程开发中避免死锁的八种方法