目录
在 Java 中,锁是一种用于实现多线程之间同步和互斥的机制。
了解
一、定义
锁是一种抽象的概念,用于控制对共享资源的访问。它确保在同一时间只有一个线程可以访问被锁保护的代码块或对象。Java 中的锁可以是显式的(如通过ReentrantLock
类实现)或隐式的(如使用synchronized
关键字)。
二、产生
-
多线程编程的需求:
- 在多线程环境下,多个线程可能同时访问共享资源。如果没有适当的同步机制,可能会导致数据不一致、竞态条件等问题。为了解决这些问题,Java 引入了锁的概念。
- 例如,一个银行账户对象可能被多个线程同时访问来进行存款和取款操作。如果没有锁来控制对账户余额的访问,可能会出现错误的结果。
-
语言设计的一部分:
- Java 语言的设计者认识到多线程编程的复杂性和潜在问题,因此在语言中提供了内置的同步机制,如
synchronized
关键字和java.util.concurrent.locks
包中的显式锁。这些机制使得开发人员能够更容易地实现线程安全的代码。
- Java 语言的设计者认识到多线程编程的复杂性和潜在问题,因此在语言中提供了内置的同步机制,如
三、作用
-
保证线程安全:
- 锁确保了对共享资源的互斥访问,防止多个线程同时修改同一数据,从而保证了数据的一致性。
- 例如,在一个多线程的计数器程序中,使用锁可以确保在多个线程同时增加计数器值时,不会出现错误的结果。
-
避免竞态条件:
- 竞态条件是指多个线程同时访问和修改共享资源时,由于执行顺序的不确定性而导致的错误结果。锁可以避免竞态条件的发生,通过确保线程在访问共享资源时的正确顺序。
- 例如,在一个生产者 - 消费者问题中,使用锁可以确保生产者在向缓冲区添加数据时,消费者不会同时从缓冲区中取出数据,从而避免了缓冲区的混乱。
-
实现线程间的同步:
- 锁不仅可以用于互斥访问,还可以用于实现线程之间的同步。例如,一个线程可以等待另一个线程完成某个任务后再继续执行。
- 例如,在一个多线程的任务处理系统中,一个主线程可以使用锁来等待所有子线程完成任务后再进行下一步操作。
总之,Java 中的锁是一种重要的多线程编程机制,它可以帮助开发人员实现线程安全的代码,避免竞态条件的发生,并实现线程之间的同步。
总览
序号 | 锁名称 | 应用 |
---|---|---|
1 | 乐观锁(Optimistic Locking) | CAS |
2 | 悲观锁(Pessimistic Locking) | synchronized、vector、hashtable |
3 | 同步锁 | synchronized |
4 | 互斥锁(Mutex) | synchronized、Reentrantlock |
5 | 公平锁(FairLock) | Reentrantlock(true) |
6 | 非公平锁 | synchronized、Reentrantlock(false) |
7 | 自旋锁 | CAS |
8 | 可重入锁(ReentrantLock) | synchronized、Reentrantlock、Lock |
9 | 读写锁(ReadWriteLock) | ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet |
10 | 共享锁 | ReentrantReadWriteLock中读锁 |
11 | 独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中写锁 |
12 | 偏向锁(Biased Locking) | 锁优化技术 |
13 | 重量级锁 | synchronized |
14 | 轻量级锁 | 锁优化技术 |
15 | 分段锁 | concurrentHashMap |
乐观锁
-
是一种乐观思想,抽象的锁,认为读多写少,每次拿数据时默认对方不会修改数据。
-
但是会在更新的时候进行判断期间是否有其他线程对其进行修改,写之前读取当前版本号,一样则更新,不一样重复 读-比较-写 的操作。
-
基本通过CAS实现。CAS是一种原子操作,比较当前值跟传入值一样则更新,否则失败
悲观锁
-
是一种悲观思想,抽象的锁,认为写多读少,每次拿数据都认为别人会修改。
-
所以每次读取时都会上锁,这样别人想读写这个数据时会Block至拿到锁
-
JAVA中悲观锁就是synchronized;AQS(AbstractQueuedSynchronizer)框架下的锁先尝试CAS乐观锁去获取锁,获取不到才转换为悲观锁,如RetreenLock
互斥锁和同步锁
都是悲观锁
同步锁在很多情况下可以理解为是一种互斥锁,但它们并不完全等同。
一、相似之处
-
互斥性:
-
同步锁和互斥锁都具有互斥的特性,即在同一时间只允许一个线程访问被保护的资源或代码区域。
-
例如,使用
synchronized
关键字修饰的方法或代码块,以及使用ReentrantLock
实现的锁,都能确保在同一时刻只有一个线程能够执行被锁保护的代码,这与互斥锁的行为一致。
-
-
保证数据一致性:
-
两者都是为了防止多个线程同时访问共享资源时出现数据不一致的问题。通过对资源的互斥访问,确保线程在修改共享数据时不会被其他线程干扰。
-
二、不同之处
-
概念范围:
-
同步锁更侧重于实现线程之间的同步操作,确保线程按照特定的顺序执行或访问共享资源。同步可以包括多种方式,如互斥锁只是其中一种实现同步的手段。
-
例如,除了互斥锁,还可以通过条件变量、信号量等方式实现线程同步,但这些方式不一定完全等同于互斥锁。
-
-
实现方式:
-
同步锁可以通过多种方式实现,如
synchronized
关键字、ReentrantLock
等。这些实现方式在某些方面可能有不同的特性和功能。 -
互斥锁通常更强调对资源的独占访问,实现方式相对较为单一,主要是通过类似
ReentrantLock
这样的明确的互斥锁机制来实现。
-
综上所述,同步锁和互斥锁有很多相似之处,但在概念范围和实现方式上存在一些差异。在很多情况下,同步锁可以表现为互斥锁的行为,但不能简单地认为同步锁就是互斥锁。
公平锁
加锁前检查有没有排队等待的线程,优先排队等待的线程,先来先得
多核情况需要维护一个队列,性能低于非公平锁
非公平锁
加锁时,不考虑排队情况,都尝试获取锁
性能高于公平锁5-10倍
synchronized只能非公平锁,ReentrantLock默认非公平锁
自旋锁
-
简单来说就是线程自旋不断尝试获得所需锁。
-
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗
-
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
自适应自旋锁
-
1.6引入,自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,将线程上下文切换所需的时间作为一个参考标准。大于上下文切换所需时间,那就避免自旋,直接进入堵塞等待状态,避免CPU资源损耗。
可重入锁(递归锁)
同一线程可多次请求同一把锁,不会死锁被堵塞。
ReentrantLock 和 synchronized 都是 可重入锁
假设有一个同步方法 methodA
,在其内部又调用了自身 methodA
。如果使用的锁是可重入锁,那么同一个线程在第一次获取锁进入 methodA
后,再次调用 methodA
时可以再次获取到锁,而不会被阻塞。
ReadWriteLock读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制
共享锁
也被成为读锁,允许多个线程同时获得锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
独占锁
-
每次只能一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
-
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性
关于独占锁中的“读读冲突” “读读冲突”并非指单纯的读操作之间会直接导致数据不一致的冲突。在独占锁的场景下,所谓的“读读冲突”更多是从锁的机制和资源竞争的角度来理解的。 当使用独占锁时,无论线程进行的是读操作还是写操作,锁的规则都是一次只允许一个线程获取到锁。 即使多个线程都是只读操作,如果一个线程获取了独占锁,其他线程也只能等待。这是因为独占锁的设计原则就是在同一时间只允许一个线程持有锁来访问被保护的资源或代码区域。 例如,假设系统中有一个共享的数据结构,虽然读操作不会改变其数据,但如果多个读线程同时进行操作,可能会存在一些潜在的问题。比如,可能会导致频繁的缓存失效,或者在一些复杂的并发环境中,可能会引发一些难以预测的时序问题。 再比如,从锁的实现和管理角度来看,为了确保锁的机制简单且可靠,独占锁不会区分读操作和写操作,统一按照独占的方式进行处理。 所以,在独占锁的机制下,即使只是读操作,一旦一个线程获取了锁,其他线程也需要等待,以保证锁的管理和资源访问的有序性和确定性。
偏向锁
-
只有一个线程执行,锁会偏向给线程,避免多次CAS造成资源浪费。
-
只需要每次置换ThreadID(线程ID)的时候依赖一次CAS原子指令
偏向锁是 Java 虚拟机中的一种锁优化机制。 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁就是基于这种假设,当一个线程获取锁时,会将锁标记为偏向于这个线程,下次该线程再次请求这个锁时,无需再进行同步操作,从而提高性能。 偏向锁的主要目的是在没有多线程竞争的情况下,减少不必要的同步开销。 应用场景主要包括以下几种: 1. 单线程环境:如果程序在大多数情况下都是单线程运行,例如一些只在初始化阶段进行配置,后续主要由单个线程操作的对象。 2. 线程局部对象:某些对象的操作主要局限于特定的线程,其他线程很少甚至不会访问。 例如,在一个日志处理程序中,有一个专门用于记录日志信息的对象,大部分时间只有一个线程在操作它来写入日志,这种情况下就适合使用偏向锁来提高性能。 需要注意的是,偏向锁在存在多线程竞争时会升级为轻量级锁或重量级锁,以保证线程安全。
轻量级锁
-
多个线程交替执行,但是不针对同一对象,通过自旋等待获取锁,避免上下文切换造成的资源消耗
-
如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
重量级锁
-
多个线程并发执行,且针对同一对象,避免自旋时间过久,也就是无用自旋造成的资源浪费
-
通过对象内部监视器锁(monitor)来实现。监视器锁又是依赖操作系统(互斥量)Mutex Lock来实现。
-
操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间
分段锁
-
分段锁将一个数据结构或资源划分成多个段(segment),每个段都有自己独立的锁。这样,不同的线程可以同时访问不同段上的数据,而不会相互阻塞,从而提高并发度。
-
锁的设计,并不是具体的一种锁,像 ConcurrentHashMap就使用到了
锁优化
减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。
锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互
斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如
LinkedBlockingQueue 从头部取出,从尾部放数据
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。锁粗化是一种优化技术,指的是在程序执行过程中,当一系列连续的对同一锁的操作被检测到时,虚拟机可能会将这些锁操作合并为一个范围更大的锁操作,以减少锁获取和释放的次数,从而提高性能。
锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。