0

Java 的线程安全,以及死锁

 1 year ago
source link: https://www.boris1993.com/java-thread-security-deadlock.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

乐观锁与悲观锁

  • 乐观锁:认为在使用数据时,不会有别的线程修改数据,所以不会加锁,只在更新时判断之前有没有被别的线程更新了数据。比如在数据库中设置一个 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 ' ')

示例输出:

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.
  • 以确定的顺序获锁
  • 尽量降低锁的使用粒度
  • 尽量使用同步代码块,而不是同步方法
  • 避免嵌套锁

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK