Java中锁的分类
1.内置锁:
1.使用关键字synchronized实现。
2.非公平锁,可重入锁,可以对方法或代码块进行同步,被同步的代码同一时刻只有一个线程可以执行其中的代码。
2.显式锁:
1.使用java.util.concurrent.locks包下锁机制实现,比如ReentrantLock。
2.提供了更加灵活的控制,增加了轮询、超时、中断等高级功能,需要显式的用lock()方法加锁和unlock()方法释放锁,它是基于AQS实现的锁,他支持公平锁和非公平锁,同时他也是可重入锁和自旋锁。
3.条件锁:
1.使用java.util.concurrent.locks包下的Condition接口和ReentrantLock实现
2.允许线程在某个特定条件满足时等待或唤醒
4.读写锁:
1.使用java.util.concurrent.locks包下的ReentrantReadWriteLock实现。
2.允许多个线程同时读共享资源,但只允许一个线程进行写操作。
5.StampedLock:
1.在Java8中引入的新型锁机制,也是在java.util.concurrent.locks包下。
2.提供了三种模式:写锁、悲观读锁和乐观读锁。
6.无锁
1.也就是我们常说的乐观锁,基于原子操作实现的锁机制,比如CAS算法。
2.避免了传统锁机制的线程阻塞和切换开销。
常见锁的解释
1.乐观锁:
1.乐观锁是一种并发控制策略,它假设对共享资源的访问不会发生冲突,因此在进行读取操作时不会加锁。只有在尝试更新数据时,才会检查自上次读取以来该数据是否被其他线程修改过。如果数据未被修改,则执行更新(更新期间加锁,保证是原子性的);如果数据已经被修改,则采取相应的措施,如回滚、重试或通知用户等。乐观锁通常使用版本号、时间戳或 CAS(Compare-And-Swap)等技术来实现。
2.java中的乐观锁:CAS比较与替换,比较当前值(主内存中的值),与预期值(当前线程中的值,之内存中值的一份拷贝)是否一样,一样就更新,否则继续进行CAS。
2.悲观锁
1.悲观锁是一种悲观思想,假设冲突会经常发生,因此在访问共享资源时总是先加锁。认为当前环境是写多读少,遇到并发写的可能性很高,每次去拿数据的时候都认为其他线程会修改,所以每次读写都会上锁。这种方式可以防止多个线程同时修改共享资源,但可能会导致性能问题,特别是在高并发环境下,因为线程需要等待锁的释放。
2.Java中的悲观锁:synchronized修饰的方法和方法块、ReentrantLock
3.自旋锁
自旋锁是一种技术:为了让线程等待,我们只只需让线程执行一个忙循环(自旋),能让俩个以上的线程同时并行执行,就可以让后面请求锁的那个线程多“等待一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。
自旋锁的优点:避免了线程切换的开销(避免了线程上下文的切换),挂起和恢复线程的操作都需要转入内核态中完成,这些操作给jvm的并发性带来了压力。
自旋锁的缺点:占用处理器时间,如果占用时间太长,会白白消耗处理器资源,不会做任何有价值的工作,所以自旋等待的时间一定要有一个限度
自旋默认的次数是:10次,可以自行更改。
4.可重入锁
可重入锁是一种技术,任一线程在获取到锁之后能够再次获取该锁而不会被锁阻塞。
原理:如果当前获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取锁,进行计数+1。释放锁:释放锁时,记性计数-1.
注意:可重入锁只能解决或许同一锁的死锁问题。面对的是同一把锁的可重入问题。
获取了多把锁但没有全部释放掉会导致层序卡死,线程出不来。
获取了n把锁但释放了n+1把锁程序会报错。
5.读写锁
读写锁是一种技术,通过ReetrantReadWriterLock类来实现,为了提高性能,java提供了读写锁,在读的地方使用读锁,写的地方使用写锁。读是没有阻塞的,多个读锁不互斥,读锁和写锁互斥,这是由JVm自己控制的。
读锁:允许多个线程获取读锁,同时访问同一资源。
写锁:只允许一个线程或许写锁,不允许同时访问同一资源。
6.公平锁:
公平锁是一种思想:多个线程按照申请的锁的顺序来获取锁。
该锁会维护一个等待队列,每个线程会查看每个锁的等待队列,如果等待队列为空,就获取锁,如果不为空就放入队列末尾,按照FIFO的原则从队列拿到线程,然后占有锁。
7.非公平锁:
非公平锁是一种思想,多个线程获取锁,不是先来的先得到锁,谁抢上谁就拥有锁。
优点:非公平锁的性能高于公平锁。
缺点:有可能某个线程长时间获取不到锁。
8.共享锁和独占锁
共享锁:以共享的方式持有锁,和乐观锁读写锁一样。
独占锁:只能有一个线程获取锁,和悲观锁和互斥锁同意。
9.重量级锁:
重量级锁是一种称谓:sysnchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器本身依赖底层的操作系统的Mutex Lock(互斥锁)来实现的,操作系统实现线程的切换需要从用户态到核心态,成本非常高,这种依赖于操作系统Mutex Lock来实现锁的称为重量级锁,为了优化synchronized,引入了轻量级锁和偏向锁。
10.轻量级锁:
轻量级锁是一种锁的优化机制,是指在没有产生竞争的情况下使用CAS操作去消除同步使用的互斥量,就是说如果线程之间没有产生竞争的关系,就不需要用户态到内核态的频繁切换,可以提升性能。
如果出现俩个以上的线程共同竞争一把锁,就会导致线程无休止的自旋,导致cpu资源的浪费,当到达一定的自旋次数就会将此线程阻塞并挂起,等待被唤醒。
如果出现了竞争并且不升级为重量锁的话,轻量级锁会比重量级锁更慢。
11.偏向锁
没有与当前线程竞争的情况,就是说该锁只有被这一个线程来回获取就是偏向锁。
偏向锁也是一种优化机制。
12.互斥锁:
互斥锁和乐观锁和读锁有不同点,读锁是多个线程都可以读取数据,而互斥锁任和情况下也能单个线程就能访问。和悲观锁和独占锁同意。
13.同步锁
同步锁和互斥锁同义,在并发情况下执行的多个线程,在同一时间只允许一个线程访问共享数据。
14.锁粗化
锁粗化是一种优化技术,如果一系列的操作都是对同义操作进行的反复加锁和解锁,造成性能的消耗,一种解决方案是将锁的范围扩大化到整个序列的外部,这样加锁的频率就是大大降低。减少性能消耗。
15.CAS
1.CAS是一种原子操作,全称叫做(Compare-And-Swap)比较与交换,是一种轻量级的同步机制,它允许线程在不使用传统锁的情况下进行无锁更新。
CAS 操作包含三个参数:内存位置 V、预期原值 A 和新值 B。当且仅当内存位置 V 的值等于预期原值 A 时,CAS 才会将这个位置的值更新为新值 B,否则不执行任何操作。无论更新是否成功,CAS 都会返回内存位置 V 的当前值。
2.CAS 的特点:
非阻塞:CAS 不会引起线程阻塞,即使多个线程同时竞争同一资源,也不会导致线程挂起。
忙等待:如果 CAS 失败,通常线程会继续循环尝试,直到成功为止,这可能会导致 CPU 使用率升高。
乐观锁的实现:CAS 经常用于实现乐观锁,因为它假设冲突很少发生,并在更新时才检查是否有冲突。
synchronized和ReentrantLock
1. synchronized
简介:
synchronized
是 Java 语言的关键字,用于实现悲观锁。它可以通过修饰方法或代码块来确保在同一时刻只有一个线程可以执行被 synchronized
修饰的代码段。synchronized
提供了对对象或代码块的独占锁,确保线程安全。
在早期的 Java 版本中,synchronized 主要依赖于操作系统级别的互斥锁(mutex)。然而,从 Java 6 开始,JVM 对 synchronized 进行了大量优化,引入了偏向锁、轻量级锁和重量级锁等机制,以提高性能。
偏向锁:当一个线程第一次获取锁时,JVM 会将锁“偏向”给该线程,避免后续的同步开销。只有当其他线程尝试获取同一个锁时,才会升级为轻量级锁。
轻量级锁:轻量级锁使用了 CAS 操作来实现锁的竞争。当多个线程竞争同一个锁时,JVM 会尝试使用 CAS 来更新锁的状态,而不是立即升级为重量级锁。
重量级锁:如果 CAS 竞争失败,JVM 会将锁升级为重量级锁,这时线程会被阻塞,进入操作系统级别的调度
使用方式:
- 方法级别:当
synchronized
关键字修饰一个实例方法时,该方法在被调用时会自动锁定当前对象(this
)。 - 代码块级别:可以指定一个明确的对象作为锁,只对特定的代码段进行同步。
public class Example {
// 同步方法
public synchronized void methodA() {
// 同步代码
}
// 同步代码块
public void methodB() {
synchronized (this) {
// 同步代码
}
}
}
特点:
- 内置支持:
synchronized
是 Java 语言的内置特性,使用起来非常简单,适用于大多数同步需求。 - 自动释放:当线程退出
synchronized
方法或代码块时,锁会自动释放,不需要显式调用解锁操作。 - 阻塞式:如果一个线程无法获取锁,它会被阻塞,直到锁被释放。
- 不可中断:一旦线程进入等待状态(如等待锁),它是不可中断的。如果需要中断等待中的线程,通常会导致
InterruptedException
被忽略。 - 不支持公平锁:
synchronized
默认是非公平锁,即它不保证线程按照请求锁的顺序获得锁。 - 性能优化:从 Java 6 开始,JVM 对
synchronized
进行了大量优化,引入了偏向锁、轻量级锁和重量级锁等机制,显著提高了其性能。
适用场景:
- 简单的同步需求:如果你只需要基本的线程安全机制,
synchronized
是一个很好的选择,因为它简单易用,不容易出错。 - 读多写少的场景:
synchronized
在读多写少的场景下表现良好,特别是在 JVM 的优化下,它的性能已经非常接近ReentrantLock
。
2. ReentrantLock
简介:
ReentrantLock
是 java.util.concurrent.locks
包中的一个类,提供了比 synchronized
更灵活的锁机制。它是一个显式的锁类,允许开发者手动获取和释放锁,并提供了更多的功能和控制选项。
使用方式:
- 获取锁:通过
lock()
方法获取锁。 - 释放锁:通过
unlock()
方法释放锁。必须在finally
块中释放锁,以确保即使发生异常也能正确释放锁。
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 同步代码
} finally {
lock.unlock(); // 确保锁被释放
}
}
}
特点:
- 显式锁管理:
ReentrantLock
需要手动获取和释放锁,这提供了更大的灵活性,但也增加了复杂性。如果不小心忘记释放锁,可能会导致死锁或其他并发问题。 - 支持公平锁:
ReentrantLock
可以通过构造函数参数指定是否为公平锁。公平锁保证线程按照请求锁的顺序获得锁,避免某些线程长期得不到锁的情况。 - 可中断等待:
ReentrantLock
提供了lockInterruptibly()
方法,允许线程在等待锁的过程中被中断,从而抛出InterruptedException
。这对于处理长时间等待的情况非常有用。 - 尝试获取锁:
ReentrantLock
提供了tryLock()
方法,可以在尝试获取锁时不阻塞,或者设置超时时间来尝试获取锁。这使得程序可以根据情况选择是否继续等待。 - 锁的状态查询:
ReentrantLock
提供了多种方法来查询锁的状态,例如isHeldByCurrentThread()
、getHoldCount()
等,便于调试和监控。 - 读写锁支持:
ReentrantLock
还有一个相关的类ReentrantReadWriteLock
,允许多个读线程同时访问资源,但在写线程持有锁时不允许任何其他线程访问。这对于读多写少的场景非常有用。
适用场景:
- 复杂的同步需求:如果你需要更多的控制和灵活性,比如公平锁、可中断等待、尝试获取锁等功能,
ReentrantLock
是更好的选择。 - 高性能要求:在某些情况下,
ReentrantLock
可能比synchronized
提供更好的性能,特别是在需要高级锁特性的场合。 - 读多写少的场景:
ReentrantReadWriteLock
可以显著提高读多写少场景下的性能,因为它允许多个读线程同时访问资源。
3. synchronized
和 ReentrantLock
的主要区别
特性 | synchronized | ReentrantLock |
---|---|---|
锁的获取方式 | 自动获取和释放锁,无需手动管理 | 需要显式地调用 lock() 和 unlock() 方法 |
锁的类型 | 内置关键字,基于 JVM 实现 | 显式的锁类,基于 java.util.concurrent.locks 包 |
公平锁支持 | 不支持公平锁 | 支持公平锁(通过构造函数参数指定) |
可中断等待 | 不支持可中断等待 | 支持可中断等待(lockInterruptibly() ) |
尝试获取锁 | 无法尝试获取锁而不阻塞 | 支持 tryLock() ,可以在不阻塞的情况下尝试获取锁 |
锁的状态查询 | 无法直接查询锁的状态 | 提供多种方法查询锁的状态(如 isHeldByCurrentThread() ) |
读写锁支持 | 不支持读写锁 | 支持 ReentrantReadWriteLock ,允许多个读线程同时访问资源 |
性能 | 从 Java 6 开始进行了大量优化,性能接近 ReentrantLock | 在某些复杂场景下可能提供更好的性能,特别是在需要高级锁特性时 |
使用难度 | 简单易用,不容易出错 | 更加灵活,但需要更谨慎地管理锁的获取和释放 |
4. 选择建议
synchronized
:适用于简单的同步需求,特别是当你不需要那些ReentrantLock
提供的高级特性时。它更简洁,不容易出错,适合大多数同步场景。如果你只需要基本的线程安全机制,synchronized
是一个很好的选择。ReentrantLock
:适用于需要更多控制和灵活性的复杂同步场景,比如你需要公平锁、可中断等待、尝试获取锁等功能时。ReentrantLock
提供了更强大的功能,但需要更谨慎地使用以避免潜在的问题,如死锁或忘记释放锁。
5. 总结
synchronized
是 Java 语言的内置特性,简单易用,适用于大多数同步需求,特别是在 JVM 的优化下,它的性能已经非常接近ReentrantLock
。ReentrantLock
提供了更多的功能和灵活性,适合复杂的同步需求,尤其是在需要高级锁特性(如公平锁、可中断等待、读写锁)的场景中。
公平锁和非公平锁
公平锁(Fair Lock)和非公平锁(Non-fair Lock)是两种不同的锁获取策略,它们决定了多个线程在竞争同一把锁时的处理方式。这两种锁的主要区别在于线程获取锁的顺序是否遵循先来先服务的原则。
公平锁(Fair Lock)
-
定义:公平锁保证线程按照请求锁的顺序获得锁,即先请求锁的线程会先得到锁。每个线程在进入等待队列时都会被放置在队列的尾部,并且只有当它到达队列头部并且锁可用时,才会获得锁。
-
优点:
- 避免了“饥饿”现象,确保所有线程最终都能获得锁。
- 更符合直观的公平性原则,适合那些对资源访问顺序有严格要求的应用场景。
-
缺点:
- 性能可能较低,因为每次都需要检查等待队列中的所有线程,这增加了额外的开销。
- 在高并发情况下,频繁的上下文切换可能会导致性能下降。
-
适用场景:适用于需要严格按照请求顺序执行的场景,或者当长时间持有锁可能导致其他线程长期等待的情况。
非公平锁(Non-fair Lock)
-
定义:非公平锁不保证线程按照请求锁的顺序获得锁。当一个线程尝试获取锁时,如果锁此时恰好可用,那么该线程可以立即获得锁,而无需考虑是否有其他线程已经在等待队列中。这意味着后来的线程有可能插队,在某些情况下甚至可以在早于它请求锁的线程之前获得锁。
-
优点:
- 性能通常更好,因为在大多数情况下,非公平锁减少了线程获取锁所需的时间,因为它不需要维护和检查等待队列。
- 减少了线程调度的开销,从而提高了吞吐量。
-
缺点:
- 可能会导致某些线程长期得不到锁,产生“饥饿”现象。
- 不符合直观的公平性原则,不适合对资源访问顺序有严格要求的应用场景。
-
适用场景:适用于对性能要求较高、对资源访问顺序没有严格要求的场景。由于其较高的吞吐量,非公平锁在许多实际应用中更为常见。
Java 中的实现
在 Java 中,ReentrantLock
类提供了对公平锁和非公平锁的支持。你可以通过构造函数的参数来指定锁的行为:
import java.util.concurrent.locks.ReentrantLock;
// 创建一个非公平锁(默认)
ReentrantLock nonfairLock = new ReentrantLock();
// 创建一个公平锁
ReentrantLock fairLock = new ReentrantLock(true);
在 ReentrantLock
的构造函数中传递 true
表示创建一个公平锁,而传递 false
或者不传递参数则表示创建一个非公平锁(这也是默认行为)。
总结
- 公平锁 确保线程按请求顺序获取锁,避免饥饿,但可能带来性能上的损失。
- 非公平锁 提供更高的吞吐量,但可能导致某些线程饥饿,不适合对资源访问顺序有严格要求的场景。
- 选择哪种锁取决于你的具体需求,包括性能要求、对公平性的重视程度以及应用场景的特点。
乐观锁、CAS和自旋锁
Java实现CAS的原理 | Java程序员进阶之路
CAS (Compare-And-Swap) 和 自旋锁 经常一起使用,因为 CAS 操作本身是非阻塞的,它可以在不使用传统锁的情况下实现原子操作。然而,CAS 操作并不保证一定会成功,特别是在高竞争的情况下。为了确保最终能够成功地完成所需的操作,开发者通常会在 CAS 失败后继续尝试,这就形成了所谓的自旋行为。
在这种情况下,CAS 可以看作是实现自旋锁的一种方式。具体来说,线程会在一个循环中不断地使用 CAS 尝试更新共享变量,直到成功为止。这种模式被称为“CAS 自旋”,它是非阻塞同步算法的一个常见实现。例如,在 Java 的 Atomic
类中,很多方法内部就是使用了 CAS 操作,而在高竞争情况下,这些方法可能会隐式地表现出自旋的行为。
- 乐观锁 是一种并发控制策略,假设冲突很少发生,只有在更新时才会检查冲突。
- CAS 是一种用于实现无锁算法的原子操作,它可以用来构建更高级别的同步原语或无锁数据结构。
- 自旋锁 是一种忙等待机制,它通过重复检查条件来获取锁,而不是阻塞线程。当一个线程试图获取已经被占用的自旋锁时,它会不停地执行空循环(即“旋转”),直到锁变为可用。自旋锁适合于锁竞争时间非常短的情况,因为在短时间内反复尝试获取锁的成本可能比线程阻塞和恢复的代价要低。
- CAS 与自旋锁 经常一起使用,用来自旋等待 CAS 操作的成功。
- 乐观锁是一种并发控制策略,而 CAS 是一种具体的实现技术。乐观锁可以通过多种方式实现,其中一种常见的方法就是使用 CAS 操作。
- 乐观锁 和 自旋锁 虽然不是直接相关,但在某些情况下可能会结合使用,比如先尝试乐观锁,失败后再使用自旋锁。
CAS相关问题
1、ABA问题
1.1、何为ABA
ABA问题指在CAS操作过程中,如果变量的值被改为了 A、B、再改回 A,而CAS操作是能够成功的,这时候就可能导致程序出现意外的结果。
在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他线程修改为另外的值,然后再被修改回原值,此时CAS操作会认为这个值没有被修改过,导致数据不一致。
1.2、解决方案
为了解决ABA问题,Java中提供了AtomicStampedReference类,该类通过使用版本号的方式来解决ABA问题。每个共享变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变了,版本号也会发生变化,即使共享变量被改回原来的值,版本号也不同,因此CAS操作会失败。
下面是一个使用AtomicStampedReference类解决ABA问题的示例代码:
public void test() {
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0);
// 重现ABA问题
int oldStamp = atomicStampedRef.getStamp();
int oldValue = atomicStampedRef.getReference();
// 将值从 1 改为 2,并使版本号自增
atomicStampedRef.compareAndSet(oldValue, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
// 将值从 2 改回 1,并使版本号自增
atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
// 使用旧版本号修改时,即使值和旧值一样,但版本号已经发生了变化,导致修改失败
atomicStampedRef.compareAndSet(2, 1, oldStamp, oldStamp + 1);
}
在上面的示例中,通过getStamp()和getReference()方法分别获取共享变量的版本号和值,然后使用compareAndSet()方法进行CAS操作,每次操作都会更新版本号。这样,就可以避免ABA问题。
2、CPU空转
2.1、为什么出现CPU空转
除了ABA问题,CAS操作还可能会受到自旋时间过长的影响,因为如果某个线程一直在自旋等待,会浪费CPU资源。
2.2、解决方案
为了解决上述问题,可以采用自适应自旋锁的方式,即在前几次重试时采用忙等待的方式,后面则使用阻塞等待的方式,避免浪费CPU资源。
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
private int count;
public void lock() {
Thread currentThread = Thread.currentThread();
// 已经获取了锁
if (owner.get() == currentThread) {
count++;
return;
}
// 自旋等待获取锁
while (!owner.compareAndSet(null, currentThread)) {
// 自适应自旋
if (count < 10) {
count++;
} else {
// 阻塞等待
LockSupport.park(currentThread);
}
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 当前线程持有锁
if (owner.get() == currentThread) {
if (count > 0) {
count--;
} else {
// 释放锁
owner.compareAndSet(currentThread, null);
// 唤醒其他线程
LockSupport.unpark(currentThread);
}
}
}
}
这里解释一下上面代码:
在设计时使用了AtomicReference来保存当前持有锁的线程对象,这样可以保证线程安全。
当一个线程请求获取锁时,如果当前线程已经持有锁,则将计数器加1,否则使用CAS操作来获取锁。这样可以避免了使用synchronized关键字或者ReentrantLock等锁的实现机制。
当线程获取锁失败时,使用自旋等待的方式,这样可以避免线程进入阻塞状态,避免了线程上下文切换的开销。当重试次数小于10时,使用自旋等待的方式,当重试次数大于10时,则使用阻塞等待的方式。这样可以在多线程环境下保证线程的公平性和效率。
在释放锁时,如果计数器大于0,则将计数器减1,否则将锁的拥有者设为null,唤醒其他线程。这样可以确保在有多个线程持有锁的情况下,正确释放锁资源,并唤醒其他等待线程,保证线程的正确性和公平性。