文章

Java线程安全-锁

Java线程安全-锁

线程安全 - 锁

线程安全的本质?

多个线程访问共享的资源时,一个线程对资源进行修改时,其他线程也需要对这个线程进行读或者写操作,导致数据出现错误。

为什么多线程同时访问(读写)同个变量,会有并发问题?

  • JMM(Java 内存模型) 规定了所有的变量都存储在主内存中,每个线程都有自己的工作内存;线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存
  • 线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存,将会导致其他线程看到的是变量修改前的值,导致并发问题
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递需要在自己的工作内存和主内存之间进行数据同步

锁机制的本质?

通过对共享资源进行访问限制,让同一个时间只能有一个线程可以访问资源,保证了数据的准确性。
锁的内存语义

  1. 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中
  2. 当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

锁分离和锁粗化

锁分离
读读、读写、写读、写写。只要有写锁进入才需要做同步处理,但是对于大多数应用来说,读的场景要远远大于写的场景,因此一旦使用读写锁,在读多写少的场景中,就可以很好的提供系统的性能,这就是锁分离
锁粗化
锁粗化就是多次加锁

锁分类

公平锁 & 非公平锁

公平锁
获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁。

吞吐量会下降很多,队列⾥⾯除了第⼀个线程,其他的线程都会阻塞,cpu 唤醒阻塞线程的开销会很⼤;所有的线程都能得到资源,不会饿死在队列中。

非公平锁
获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争。

可以减少 CPU 唤醒线程的开销,整体的吞吐效率会⾼点,CPU 也不必取唤醒所有线程,会减少唤起线程的数量;这样可能导致队列中间的线程⼀直获取不到锁或者⻓时间获取不到锁,导致饿死。

乐观锁和悲观锁

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于 write_condition 机制的其实都是提供的乐观锁。
Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到他拿到锁。
传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁,读锁,写锁等,都是在操作之前先上锁。
Java 中同步原语 synchronized 关键字的实现是悲观锁。

synchronized

synchronized 基础

synchronized 有什么用?

解决多线程安全问题

Java 对象的构成

在 JVM 中,对象在内存中分为三块区域:

对象头

Java 的对象头由以下三部分组成:

  1. Mark Word 标记字段
  2. Klass Point 指向类的指针
  3. 数组长度(只有数组对象才有)

当我们在 Java 代码中,使用 new 创建一个对象的时候,JVM 会在堆中创建一个 instanceOopDesc 对象,这个对象中包含了对象头以及实例数据。instanceOopDesc 的基类为 oopDesc 类,结构如下:
d2o98

Mark Word

instanceOopDesc 中的 mark 成员,允许压缩。它用于存储对象的运行时记录信息,如哈希值、GC 分代年龄 (Age)、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程 ID、偏向时间戳等。(当这个对象被 synchronized 关键字当成同步锁时,围绕这个锁的一系列操作都和 Mark Word 有关。)
Mark Word 在 32 位 JVM 中的长度是 32bit,在 64 位 JVM 中长度是 64bit。
Mark Word 在不同的锁状态下存储的内容不同,在 32 位 JVM 中是这么存的:


75tct

其中无锁和偏向锁的锁标志位都是 01,只是在前面的 1bit 区分了这是无锁状态还是偏向锁状态。

JDK1.6 以后的版本在处理同步锁时存在锁升级的概念,JVM 对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM 一般是这样使用锁和 Mark Word 的:

1
2
3
4
5
6
7
1. 当没有被当成锁时这就是一个普通的对象Mark Word记录对象的HashCode锁标志位是01是否偏向锁那一位是0
2. 当对象被当做同步锁并有一个线程A抢到了锁时锁标志位还是01但是否偏向锁那一位改成1前23bit记录抢到锁的线程id表示进入偏向锁状态
3. 当线程A再次试图来获得锁时JVM发现同步锁对象的标志位是01是否偏向锁是1也就是偏向状态Mark Word中记录的线程id就是线程A自己的id表示线程A已经获得了这个偏向锁可以执行同步锁的代码
4. 当线程B试图获得这个锁时JVM发现同步锁处于偏向状态但是Mark Word中的线程id记录的不是B那么线程B会先用CAS操作试图获得锁这里的获得锁操作是有可能成功的因为线程A一般不会自动释放偏向锁如果抢锁成功就把Mark Word里的线程id改为线程B的id代表线程B获得了这个偏向锁可以执行同步锁代码如果抢锁失败则继续执行步骤5
5. 偏向锁状态抢锁失败代表当前锁有一定的竞争偏向锁将升级为轻量级锁JVM会在当前线程的线程栈中开辟一块单独的空间里面保存指向对象锁Mark Word的指针同时在对象锁Mark Word中保存指向这片空间的指针上述两个保存操作都是CAS操作如果保存成功代表线程抢到了同步锁就把Mark Word中的锁标志位改成00可以执行同步锁代码如果保存失败表示抢锁失败竞争太激烈继续执行步骤6
6. 轻量级锁抢锁失败JVM会使用自旋锁自旋锁不是一个锁状态只是代表不断的重试尝试抢锁从JDK1.7开始自旋锁默认启用自旋次数由JVM决定如果抢锁成功则执行同步锁代码如果失败则继续执行步骤7
7. 自旋锁重试之后如果抢锁依然失败同步锁会升级至重量级锁锁标志位改为10在这个状态下未抢到锁的线程都会被阻塞
Klass Point (metadata)指向类的指针

该指针在 32 位 JVM 中的长度是 32bit,在 64 位 JVM 中长度是 64bit。
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
instanceOopDesc 中的 _metadata 成员,它是联合体,可以表示未压缩的 Klass 指针 (_klass) 和压缩的 Klass 指针。对应的 klass 指针指向一个存储类的元数据的 Klass 对象。

数组长度(只有数组对象才有)

只有数组对象保存了这部分数据。该数据在 32 位和 64 位 JVM 中长度都是 32bit。

实例数据

对象的实例数据就是在 Java 代码中能看到的属性和他们的值。

对其填充

因为 JVM 要求 Java 的对象占的内存大小应该是 8bit 的倍数,所以后面有几个字节用于把对象的大小补齐至 8bit 的倍数,没有特别的功能。

⼀个空对象占多少个字节?就是 8 个字节,是因为对⻬填充的关系,不到 8 个字节对其填充会帮我们⾃动补⻬。

synchronized(this)、synchronized(xxx.class) 及同步方法区别

this 是对象锁,xxx.class 是类锁;
类锁其实也是对象锁(锁的是 Class 对象)

类锁与对象锁的区别?

对象锁:Java 的所有对象都有一个互斥锁,这个锁由 JVM 自动获取和释放,线程进入 synchronized 方法的时候获取该对象的锁,如果有其他线程获取了这个对象的锁那么当前线程会等待;synchronized 方法正常返回或异常终止时,JVM 会自动释放对象锁。
类锁:对象锁是用来控制实例方法之间的同步,类锁是控制静态方法之间的同步;类锁锁一个概念,真实并不存在,它只是用来帮助我们理解锁定实例方法和静态方法的区别。Java 类可能有很多个对象,但是只有一个 Class 对象,也就是说类的不同实例之间共享该类的 Class 对象,Class 对象也是一个 Java 对象,只不过有点特殊而已;类锁其实就是 Class 对象的锁。类锁和对象锁不是同一个锁,一个是类的 Class 对象的锁,一个是类的实例的锁,它们不互斥。

同步方法

synchronized 应⽤在实例⽅法上时,对当前实例对象 this 加锁在字节码中是通过⽅法的 ACC_SYNCHRONIZED 标志来实现的

synchronized 实现原理

synchronized 保证方法内部或代码块内部资源的互斥访问,同一时间,同一个 monitor 监视的代码,只能有一个线程在访问。
字节码层面

  1. 代码块同步是使用一对 monitorentermonitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
  2. 同步方法是通过 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
  3. JDK6 之前,monitor 的实现完全是依靠 OS 内部的互斥锁,由于需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作

JVM 层面
调用了 OS 的同步机制,例如 Mutex、Semaphore 等
CPU 层面
使用了 CPU 的 lock 指令,如果是 CAS 操作时,还用的 cmpxhg 指令

保证线程之间对监视资源的数据同步。即,任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到自己的缓存中;任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。

synchronized 做了什么优化升级 (jdk6.0)?

JDK6.0 后,锁升级:无锁→偏向锁→轻量级锁 (包括自旋操作)→重量级锁

JDK6.0 之前 synchronized 行为

synchronized 早期是一把重量级锁,为什么说是重量级呢,因为使用的是操作系统中的 monitor 完成的,这样就牵扯到用户态切内核态 (减少线程上下文切换的频率),造成了性能开销。
但有些场景下是不需要使用 monitor 就可以保证线程安全,比如说 CAS,但是如果频繁使用 CAS 的话,会大大提升 cpu 的压力,所以会有一个锁升级的过程。

Java 对象头

synchronized 要使用对象进行加锁,就要提一下 oop 模型,oop 是 java 对象在 jvm 中的存在形式,它有对象头、实例数据、对齐填充组成。Java 对象头中又包含三部分 :Mark word,类型指针,数组长度。这里我们重点说一下Mark wordMark word 主要存放的是锁信息,GC 信息,HashCode
Mark Word 32 位不同状态的含义 (Mark Word 在 32 位 JVM 中的长度是 32bit,在 64 位 JVM 中长度是 64bit): njvpb

JDK6.0 后锁的升级优化

JDK6.0 为了减少获得锁和释放锁的性能消耗,引入了偏向锁和轻量级锁;JDK6.0 之前所有的锁都是重量级锁,JDK6.0 及之后,一个对象有 4 种锁的状态,从低到高:无锁、偏向锁、轻量级锁、重量级锁。

无锁
没有对资源进行绑定,任何线程都可以尝试去修改它
偏向锁
一旦一个线程第一次获得了监视对象,之后让监视器对象偏向这个线程,以后的多次调用则可以避免 CAS 操作。其实就是弄个变量,发现为 true 则无需再走各种加锁/解锁流程,因为在很多场景,大部分对象生命周期内都只会被一个线程锁定,使用偏向锁可以降低竞争资源的开销。

实现原理: 一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,当下次该线程进入这个同步代码块时,会去检查锁的 Mark Word 里是不是存放的是自己的线程 ID;如果是则表明该线程已经获得了锁,以后该线程在进入和退出同步代码块时不需要花费 CAS 操作来加锁和释放锁;如果不是就代表有另外一个线程来竞争这个偏向锁,这个时候该线程会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID,如果替换成功表明之前的线程不存在了,MarkWord 里面的线程 ID 为新的线程 ID,锁不会升级,仍然为偏向锁,如果替换失败表明之前的线程仍然存在,那么暂停之前的线程,将自身升级为轻量级锁,此时会按照轻量级锁的方式竞争锁。

轻量级锁(自旋锁)
轻量级锁是偏向锁升级而来的,自旋就是让 CPU 做无用功 (比如空的 for 循环),占着 CPU 不放,等待获取锁的机会,如果自旋时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。JDK 采用的是适应性自旋,即线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少,自旋失败,线程会阻塞,并且升级成重量级锁(或是存在同一时间多个线程访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁)
重量级锁
重量级锁在 JVM 中又叫对象监视器 (monitor),它很像 C 中的互斥量 Mutex,除了具备 Mutex(0/1) 互斥的功能,它还负责实现了 Semaphore(信号量) 的功能,也就是说它至少包含一个竞争锁的队列和一个信号阻塞队列 (wait 队列),前者负责做互斥,后者用来做线程同步。重量级锁本质依赖底层操作系统的互斥锁实现,操作系统实现线程之间的切换需要从用户态切换到内核态,成本非常高

锁降级

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是降级发生的条件会比较苛刻,锁降级发生在 Stop The World 期间,当 JVM 进行安全点的时候,会检查是否有闲置的锁,然后进行降级

锁对比

**优点 **缺点使用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法⽐仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销消耗适⽤于只有⼀个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提⾼了程序的响应速度如果始终得不到锁,竞争的线程使⽤⾃旋会消耗 CPU求响应时间,同步块执⾏速度⾮常快
重量级锁线程竞争不使⽤⾃旋,不会消耗 CPU线程阻塞,响应时间慢追求吞吐量,同步执⾏时间较长

从 Java 6 开始,虚拟机对 synchronized 关键字做了多方面的优化,主要目的就是,避免 ObjectMonitor 的访问,减少 “ 重量级锁 “ 的使用次数,并最终减少线程上下文切换的频率 。其中主要做了以下几个优化:锁自旋、轻量级锁、偏向锁。
mrj8b

  • 无锁:没有执行同步代码块时,此时对象是无锁状态。锁标志位是 01
  • 偏向锁:当首次进入同步代码块时,会将该对象的偏向锁的线程 ID 置为当前线程 ID,并设置偏向锁标志位为 1
  • 轻量级锁:当存在线程竞争锁时,会撤销偏向锁,升级为轻量级锁,锁标志位设置为 00,然后各个线程通过 CAS 获取锁,允许短时间内锁竞争
  • 重量级锁:当对象是轻量级锁是,各个线程通过 CAS 获取锁,当达到一定次数时,会升级成重量级锁,jdk 内部实现的自适应自旋操作。

synchronized 和 lock 的区别?

uuaes
相同点: 用来解决线程安全问题
不同点:

  1. synchronized 是 Java 的关键字,监视器 monitor 实现;而 Lock 是一个接口,通过 AQS 实现的
  2. Lock 是基于 AQS(volatile+CAS) 实现的,是乐观锁的实现;synchronized 是一种悲观锁,比较耗性能,但在 JDK1.6 做了锁机制升级优化,加入了偏向锁、轻量级锁,自旋锁及重量级锁
  3. 并发不大的情况下,synchronized 性能优于 Lock 机制;并发量大,竞争资源激烈时,Lock 性能会优于 synchronized
  4. synchronized 机制执行完相应的代码逻辑后会自动释放同步监视器(退出代码块就释放锁);而 Lock 需要手动启动同步,也需要调用 unlock() 手动释放锁
  5. synchronized 无法中断等待锁;而 Lock 可以中断
  6. Lock 可以提高多个线程进行读/写操作的效率
  7. synchronized 是非公平锁,ReentrantLock 可以配置为公平锁&非公平锁
  8. synchronized 是可重入锁,ReentranLoc 也是可重入锁

synchronized 和 volatile 的区别

JMM 内存模型:
znqbv

  • synchronized 保证可见性、有序性和原子性;volatile 保证可见性和有序性,不能保证原子性。
  • synchronized 会阻塞线程,陷入内核态;volatile 不需要锁,不会阻塞线程,更轻量级
  • volatile 关键字是无法保证原子性的,而 synchronized 通过 monitorenter 和 monitorexit 两个指令,可以保证被 synchronized 修饰的代码在同一时间只能被一个线程访问,即可保证不会出现 CPU 时间片在多个线程间切换,即可保证原子性。
  • synchronized 是无法禁止指令重排和处理器优化的;volatile 可以禁止指令重排序
  • synchronized 在 jdk1.6 版本对锁的优化;volatile 实现原理是通过在 volatile 变量的操作前后插入内存屏障的方式

wait 和 synchronized 的阻塞有什么区别?

  1. wait 必须在 synchronized 代码块或方法中
  2. wait 是让当前线程挂起直到 notify 唤醒此时会释放锁;synchronized 是没获取到锁就阻塞当前线程直到获取到锁

java 中 synchronized 和 ReentrantLock 的加锁和解锁能在不同线程吗?如果能,如何实现?

synchronized 和 ReentrantLock 在加锁期间都会记录线程号。一个纪录在对象头 一个记录在 AQS 队列。 在都说了互斥锁肯定要绑定线程的呀

volatile 关键字

volatile 的语义?

  1. 内存可见性

任意一个线程修改了 volatile 修饰的变量,其他线程可以马上识别到最新值。

  1. 禁止指令重排序:部分有序性
  2. 单个 volatile 变量原子性

单个 volatile 变量的读/写(比如 vl=l)具有原子性,复合操作(比如 i++)不具有原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class VolatileFeaturesA {
    private volatile long vol = 0L;
    /**
     * 单个读具有原子性
     */
    public long get() {
        return vol;
    }
    /**
     * 单个写具有原子性
     */
    public void set(long l) {
        vol = l;
    }
    /**
     * 复合(多个)读和写不具有原子性
     */
    public void getAndAdd() {
        vol++;
    }
}

volatile 保证了什么?如何保证的?Double Check 中的 volatile?

volatile 有两条关键的语义:

  • 可见性:保证被 volatile 修饰的变量对所有线程都是可见的,对 volatile 修饰的变量写操作需同步到主内存中,读了 volatile 的变量会导致缓存失效需重新从主内存中读取最新的值
  • 禁止重排序:禁止进行指令重排序

DCL 中的 volatile 用到了禁止重排序

什么是指令重排序?

为了使指令更加符合 CPU 的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,避开为获取⼀条指令所需数据而造
成的等待,通过乱序执⾏的技术提⾼执行效率 ,这个过程就叫做指令的重排序
不管怎么重排序,(单线程)程序的执行结果不能被改变。指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。

volatile 原理?

1、内存可见性原理

volatile 使用 Lock前缀指令 禁止线程本地内存缓存,保证不同线程之间的内存可见性。Lock 前缀的指令在多核处理器下会引发了两件事情:

  • 将当前线程本地处理器缓存放的数据写回到系统主内存
  • 当前处理器缓存行回写到主内存会导致其他处理器缓存的无效(多核处理器通过 _ 嗅探技术 _ 使自己的缓存失效,下次要修改数据时,会重新从主存中读取数据)

volatile 修饰的变量修改流程:

  1. 修改本地内存,强制刷回主内存

qai3f

  1. 强制让其他线程的工作内存失效过期

y3z0n

  1. 其他线程重新从主内存加载最新值

rzer8

2、禁止重排序(DCL 中用到)

volatile 关键字禁止指令重排序有两层意思:

  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
  • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

通过 **内存屏障指令** 来禁止特定类型的处理器重排序

1
2
3
4
5
6
7
8
9
10
11
12
private static int a;//非volatile修饰变量
private static int b;//非volatile修饰变量
private static volatile int k;//volatile修饰变量

private void hello() {
    a = 1;  //语句1
    b = 2;  //语句2
    k = 3;  //语句3
    a = 4;  //语句4
    b = 5;  //语句5
    // ...
}

变量 a,b 是非 volatile 修饰的变量,k 则使用 volatile 修饰。所以语句 3 不能放在语句 1、2 前,也不能放在语句 4、5 后。但是语句 1、2 的顺序是不能保证的,同理,语句 4、5 也不能保证顺序。 并且,执行到语句 3 的时候,语句 1,2 是肯定执行完毕的,而且语句 1,2 的执行结果对于语句 3,4,5 是可见的。

内存屏障

内存屏障类型分为四类

1、LoadLoadBarriers

指令示例:LoadA —> Loadload —> LoadB 此屏障可以保证 LoadB 和后续读指令都可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比 LoadB 先执行。

2、StoreStoreBarriers

指令示例:StoreA —> StoreStore —> StoreB 此屏障可以保证 StoreB 和后续写指令可以操作 StoreA 指令执行后的数据,即写操作 StoreA 肯定比 StoreB 先执行。

3、LoadStoreBarriers

指令示例:LoadA —> LoadStore —> StoreB 此屏障可以保证 StoreB 和后续写指令可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比写操作 StoreB 先执行。

4、StoreLoadBarriers 全能屏障,它同时具有其他 3 个屏障的效果

指令示例:StoreA —> StoreLoad —> LoadB 此屏障可以保证 LoadB 和后续读指令都可以读到 StoreA 指令执行后的数据,即写操作 StoreA 肯定比读操作 LoadB 先执行。

volatile 应用

  1. DCL 单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
    public static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {     // 代码 1
            synchronized (instance) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

AQS

AQS章节

CAS

CAS 章节

死锁

1、什么是死锁?

两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。

线程竞争的资源可以是:锁、网络连接、通知事件、磁盘、带宽等

kfw1g
死锁不能被 interrupt

2、死锁产生的 4 个必要条件?

死锁问题是由 E. G. Coffman,M. J. Elphick,A. Shoshani 在 1971 年的论文 “System Deadlocks” 提出的。并且给出了知名的4种解决方式,被称为 “ 死锁的四个必要条件 “。

1
2
3
4
5
6
This deadlock situation has arisen only because all of the following general conditions were operative:

1. Tasks claim exclusive control of the resources they require ("mutual exclusion" condition).
2. Tasks hold resources already allocated to them while waiting for additional resources ("wait for" condition).
3. Resources cannot be forcibly removed from the tasks holding them until the resources are used to completion ("no preemption" condition).
4. A circular chain of tasks exists, such that each task holds one or more resources that are being requested by the next task in the chain ("circular wait" condition).”
  • 互斥条件(mutual exclusion condition):一个资源每次只能被一个任务使用。
  • 请求和保持条件(wait for condition):一个任务因为请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件(no preemption condition):任务已经获得的资源在没有使用完之前,不能强行剥夺。
  • 循环等待条件(circular wait condition):若干任务之间形成一种头尾相接的循环等待资源关系。

3、手写死锁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class DeadThreadExample {
    // 竞争资源1
    private static final Object lock1 = new Object();
    // 竞争资源2
    private static final Object lock2 = new Object();
    public static void main(String[] args) {
        // 线程1持有lock1,再去请求持有lock2
        new Thread("线程1") {
            @Override
            public void run() {
                super.run();
                synchronized (lock1) {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "-获得lock1锁,等待lock2锁");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName() + "-获得lock2锁");
                    }
                }
            }
        }.start();
        // 线程2持有lock2,再去请求持有lock1
        new Thread("线程2") {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "-获得lock2锁,等待lock1锁");
                    synchronized (lock1) {
                        System.out.println(Thread.currentThread().getName() + "-获得lock1锁");
                    }
                }
            }
        }.start();
    }
}

输出:

1
2
线程2-获得lock2锁等待lock1锁
线程1-获得lock1锁等待lock2锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class DeadThreadExample2 {
    private static final Lock l1 = new ReentrantLock();
    private static final Lock l2 = new ReentrantLock();
    public static void main(String[] args) {
        // 线程1持有lock1,再去请求持有lock2
        new Thread("线程1") {
            @Override
            public void run() {
                super.run();
                l1.lock();
                System.out.println(Thread.currentThread().getName() + "-获得lock1锁,等待lock2锁");
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                l2.lock();
                System.out.println(Thread.currentThread().getName() + "-获得lock2锁");
                l2.unlock();
                
                l1.unlock();
            }
        }.start();
        // 线程2持有lock2,再去请求持有lock1
        new Thread("线程2") {
            @Override
            public void run() {
                super.run();
                l2.lock();
                System.out.println(Thread.currentThread().getName() + "-获得lock2锁,等待lock1锁");
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                l1.lock();
                System.out.println(Thread.currentThread().getName() + "-获得lock1锁");
                l1.unlock();
                
                l2.unlock();
            }
        }.start();
    }
}

4、死锁预防

4.1 预防死锁方案 1:获取锁的顺序一致(每个线程按照一定的顺序加锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class DeadThreadExampleFix1 {
    // 竞争资源1
    private static final Object lock1 = new Object();
    // 竞争资源2
    private static final Object lock2 = new Object();
    public static void main(String[] args) {
        // 线程1持有lock1,再去请求持有lock2
        new Thread("线程1") {
            @Override
            public void run() {
                super.run();
                synchronized (lock1) {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "-获得lock1锁,等待lock2锁");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName() + "-获得lock2锁");
                    }
                }
            }
        }.start();
        // 线程2持有lock2,再去请求持有lock1
        new Thread("线程2") {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "-获得lock1锁,等待lock2锁");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName() + "-获得lock2锁");
                    }
                }
            }
        }.start();
    }
}

4.2 预防死锁方案 2:加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

tryLock(long time, TimeUnit unit) 方法和 tryLock() 方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class DeadThreadExample3 {
    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();
    public static void deathLock() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        if (lock1.tryLock(10, TimeUnit.MILLISECONDS)) {
                            try {
                                //如果获取成功则执行业务逻辑,如果获取失败,则释放lock1的锁,自旋重新尝试获得锁
                                if (lock2.tryLock(10, TimeUnit.MILLISECONDS)) {
                                    System.out.println("Thread1:已成功获取 lock1 and lock2 ...");
                                    break;
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            } finally {
                                if (lock2.isHeldByCurrentThread()) {
                                    lock2.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                        }
                    }
                    System.out.println("Thread1:获取锁失败,重新获取---");
                    try {
                        TimeUnit.NANOSECONDS.sleep(new Random().nextInt(100));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        if (lock2.tryLock(10, TimeUnit.MILLISECONDS)) {
                            try {
                                //如果获取成功则执行业务逻辑,如果获取失败,则释放lock1的锁,自旋重新尝试获得锁
                                if (lock1.tryLock(10, TimeUnit.MILLISECONDS)) {
                                    System.out.println("Thread2:已成功获取 lock2 and lock1 ...");
                                    break;
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            } finally {
                                if (lock1.isHeldByCurrentThread()) {
                                    lock1.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock2.isHeldByCurrentThread()) {
                            lock2.unlock();
                        }
                    }
                    System.out.println("Thread2:获取锁失败,重新获取---");
                    try {
                        TimeUnit.NANOSECONDS.sleep(new Random().nextInt(100));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            deathLock();
        }
    }
}

4.3 预防死锁方案 3:尝试一次性获取所有锁,获取不到就全部释放锁

tryLock() 方法:尝试获取一把锁,如果获取成功返回 true,如果还拿不到锁,就返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class DeadThreadExampleFix2 {
    private static Lock No13 = new ReentrantLock();//第一个锁
    private static Lock No14 = new ReentrantLock();//第二个锁
    // 先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
    private static void fisrtToSecond() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (No13.tryLock()) {
                System.out.println(threadName + " get 13");
                try {
                    if (No14.tryLock()) {
                        try {
                            System.out.println(threadName + " get 14");
                            System.out.println("fisrtToSecond do work------------");
                            break;
                        } finally {
                            No14.unlock();
                        }
                    }
                } finally { // 如果获取不到No14锁,则直接掉No13锁
                    No13.unlock();
                }
            }
            // Thread.sleep(r.nextInt(3)); // 解决活锁
        }
    }
    //先尝试拿No14锁,再尝试拿No13锁,No13锁没拿到,连同No14锁一起释放掉
    private static void SecondToFisrt() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (No14.tryLock()) {
                System.out.println(threadName + " get 14");
                try {
                    if (No13.tryLock()) {
                        try {
                            System.out.println(threadName + " get 13");
                            System.out.println("SecondToFisrt do work------------");
                            break;
                        } finally {
                            No13.unlock();
                        }
                    }
                } finally {  // 如果获取不到No34锁,则直接掉No14锁
                    No14.unlock();
                }
            }
            // Thread.sleep(r.nextInt(3)); // 解决活锁
        }
    }
    private static class TestThread extends Thread {
        private String name;

        public TestThread(String name) {
            this.name = name;
        }

        public void run() {
            Thread.currentThread().setName(name);
            try {
                SecondToFisrt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread.currentThread().setName("->>>线程1");
        TestThread testThread = new TestThread("<<<-线程2");
        testThread.start();
        try {
            fisrtToSecond();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5、单线程也会死锁

先获取到了一把不可重入锁,在还未释放锁之前再次获取这个不可重入锁,就会发生死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DeadThreadExample4 {

    private static final Lock lock1 = new SelfLock(); // 如果是可重入锁就不会死锁:ReentrantLock

    public static void main(String[] args) throws InterruptedException {
        new Thread("线程1") {
            @Override
            public void run() {
                super.run();
                lock1.lock();
                System.out.println(Thread.currentThread().getName() + "-第一次获得lock1锁,等待lock1锁");
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lock();
                System.out.println(Thread.currentThread().getName() + "-第鹅翅获得lock1锁");
                lock1.unlock();

                lock1.unlock();
                System.out.println("任务完成");
            }
        }.start();
    }
}

6、死锁检测工具

6.1 jstack

jstack 是 java 虚拟机自带的一种堆栈跟踪工具。jstack 用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息。 jstack 工具可以用于生成 java 虚拟机当前时刻的线程快照。线程快照是当前 java 虚拟机内每一条线程正在执行方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

  1. 通过 jps 确定当前执行任务的进程号
  2. jstack -l 44004 查看进程 44004 线程堆栈,发现了一个死锁

qh4dz

活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决:
每个线程休眠随机数,错开拿锁的时间

线程饥饿

低优先级的线程,总是拿不到执行时间

面试题

synchronized 面试回答模板

  1. synchronized 是什么?what

Java 中的关键字,锁,有同步代码块,同步方法

  1. synchronized 用来解决什么问题?how

解决线程安全问题

  1. 为什么会有线程安全的问题?

多个线程访问同一个共享资源就会存在线程安全;JMM 中规定了每个线程都有各自的工作内存,共享变量存储在主内存,当多个线程同时操作共享变量时,就会出现线程安全问题。 用 synchronized 或 Lock 来解决

  1. JMM 存在的意义背景?

JMM 用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果

  1. JMM 三大特性,Java 中如何解决这 3 个问题?
    1. 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java 中可用锁、volatile 及 final(未发生 this 逃逸) 来保证
    2. 有序性:程序执行的顺序按照代码的先后顺序执行。Java 中的 volatile 和锁可以保证
    3. 原子性:一个操作要么都执行成功,要么都失败。Java 中的锁可以保证
  2. 由此引出 volatile,具体见 volatile 回答模板
  3. synchronized 锁的实现原理
    1. 同步块实现原理:在字节码中,同步代码块开始位置 monitorenter 和同步代码块结束或异常位置添加 monitorexit
    2. 同步方法实现原理:在字节码中,添加 ACC_SYNCHRONIZED 标记
    3. Java 对象头组成
      1. Mark Word 32 位/64 位,保存了锁的信息、hashcCode 等信息
  4. synchronized 锁升级的过程
    • 无锁
    • 偏向锁
    • 轻量级锁
    • 重量级锁
  5. synchronized 和 Lock 的区别?使用场景?
  6. synchronized 和 CAS 的区别?
  7. synchronized 和 volatile 的区别?分别使用场景?

volatile 面试回答模板

  1. volatile 是什么?what
  2. 先讲 JMM 是什么?存在的一些问题?
  3. volatile 的内存语义?可用来做什么?how
    1. 可见性
    2. 指令重排序
    3. 单个变量的原子性
  4. volatile 实现可见性的原理
  5. volatile 实现指令重排序的原理
  6. volatile 和 synchronized 的区别
  7. volatile 的使用场景?

CAS 回答模板

什么是 CAS?
CAS 全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁。
CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。
CAS 操作数

  • 内存地址 V
  • 期望的值 A
  • 新修改的 B

CAS 原理
CAS 是通过 JDK 提供的 UnSafe 实现的,CAS 底层会根据操作系统和处理器的不同来选择对应的调用代码,以 Windows 和 X86 处理器为例,如果是多处理器,通过带 lock 前缀的 cmpxchg 指令对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作;如果是单处理器,通过 cmpxchg 指令完成 比较+更新 原子操作

通过 CPU 的 cmpxchg 指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过 Java 代码中的 while 循环再次调用 cmpxchg 指令进行重试,直到设置成功为止。

CAS 都是硬件级别的操作,因此效率会比普通的加锁高一些。
CAS 不足

  • ABA 问题,加版本号,AtomicStampedReference
  • 自旋时间过长,CPU 开销过大
  • 只能保证单个变量原子操作,多个变量 AtomicReference

DCL 中 volatile 的作用是什么?不加 volatile 的问题?

在程序执行过程中,JVM 为了速率,有可能会产生重排序。初始化一个实例来讲,他的过程如下:

  1. 分配内存空间;
  2. 初始化对象(执行构造方法);
  3. 将引用实例指向该内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
    // Singleton对象属性,加上volatile关键字是为了防止指定重排序,要知道singleton = new Singleton()拆分成cpu指令的话,有足足3个步骤
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        // 两层判空,第一层是为了避免不必要的同步
        if (instance == null) { 
            synchronized (Singleton.class) {
                if (instance == null) { // 第二层是为了在null的情况下创建实例//
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

singleton = new Singleton() 这段代码其实不是原子性的操作,它至少分为以下 3 个步骤:

  1. 分配内存空间:给 singleton 对象分配内存空间
  2. 初始化对象:调用 Singleton 类的构造函数等,初始化 singleton 对象
  3. 引用指向分配的空间:将 singleton 对象指向分配的内存空间,这步一旦执行了,那 singleton 对象就不等于 null 了

正常情况下,singleton = new Singleton() 的步骤是按照 1->2->3 这种步骤进行的,但是一旦 JVM 做了指令重排序,那么顺序很可能编程 1->3->2,如果是这种顺序,可以发现,在 3 步骤执行完 singleton 对象就不等于 null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton 不等于 null 了,就这样把半成品的实例返回去,调用是会报错的。

出现指令重排序的图:
pkz3t
出现了指令重排序后,按照上图的流程逻辑,很可能会返回还没完成初始化的 singleton 对象,导致使用这个对象时报错,而 volatile 关键字的作用之一就是禁止指令重排序。
为什么要加 volatile?
加 volatile 是为了禁⽌指令重排。指令重排指的是在程序运⾏过程中,并不是完全按照代码顺序执⾏的,会考虑到性能等原因,将不影响结果的指令顺序有可能进⾏调换。
DCL 使用 volatile 关键字,是为了禁止指令重排序,避免返回还没完成初始化的 singleton 对象,导致调用报错,也保证了线程的安全

AQS 回答模板

  1. 什么是 AQS?
  2. AQS 解决什么问题?
  3. AQS 原理?

死锁回答模板

造成死锁的四个条件

  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

如何打破死锁?
把 4 个条件中一个打破即可

Ref

本文由作者按照 CC BY 4.0 进行授权