Java 的线程安全,以及死锁

刚才面试的时候被问到了关于线程安全和死锁的问题,有点露怯,故赶紧查漏补缺,记录于此。

线程安全

线程安全是程序设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。

乐观锁与悲观锁

  • 乐观锁:认为在使用数据时,不会有别的线程修改数据,所以不会加锁,只在更新时判断之前有没有被别的线程更新了数据。比如在数据库中设置一个 version 字段,在更新前先查询该字段的值,然后在写入时比较数据库中的值是否与之前查询到的值相同。
  • 悲观锁:认为自己在使用数据的时候,一定有别的线程来修改数据,因此在获取数据的时候先加锁,确保数据不会被线程修改。

如何保证线程安全

  • syncronized 关键字,举例:ConcurrentHashMap。是悲观锁。
    • 锁升级机制:

      它是指在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM 让其持有偏向锁,并将 threadid 设置为其线程 ID,再次进入的时候会先判断 threadid 是否与其线程 ID 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

      • 偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的 id 会记录在对象的 Mark Word 中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
      • 轻量级锁(CAS):就是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过 CAS 操作尝试将 Mark Word 更新为指向 LockRecord 的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
      • 重量级锁:虚拟机使用 CAS 操作尝试将 MarkWord 更新为指向 LockRecord 的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查 MarkWord 是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadLockExample$2.run(DeadLockExample.java:58)
- waiting to lock <0x000000076ab660a0> (a java.lang.Object)
- locked <0x000000076ab660b0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at DeadLockExample$1.run(DeadLockExample.java:28)
- waiting to lock <0x000000076ab660b0> (a java.lang.Object)
- locked <0x000000076ab660a0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

避免死锁

  • 以确定的顺序获锁
  • 超时放弃
  • 死锁检测
  • 尽量降低锁的使用粒度
  • 尽量使用同步代码块,而不是同步方法
  • 避免嵌套锁
  • 专锁专用

参考文章

  • 4 种解决线程安全问题的方式
  • Java 高级教程系列 - 死锁示例及解决
  • Java 多线程开发中避免死锁的八种方法