---
title: "Java的线程安全，以及死锁"
source: https://www.boris1993.com/java-thread-security-deadlock.html
date: 2022-12-14
updated: 2026-10-10
tags: [Java, 多线程, 线程安全, 死锁]
categories: [学知识]
---

# 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 ' ')`。

示例输出：

```plaintext
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多线程开发中避免死锁的八种方法
