文章目录
- 概要
- java中的锁-上正文
- 概念
- 常见问题:
- 1、死锁(Deadlock):
- 2、活锁(Livelock):
- 3、饥饿(Starvation):
- 4、优先级反转(Priority Inversion):
- 5、上下文切换开销(Context Switch Overhead):
- 6、性能瓶颈(Performance Bottlenecks):
- 7、锁颗粒度不当(Improper Lock Granularity):
- 8、忘记释放锁(Forgetting to Release Locks):
- 9、不适当的锁类型选择(Choosing the Wrong Type of Lock):
- 10、嵌套锁(Nested Locks):
- 11、无限期等待(Indefinite Waiting):
概要
提示:
锁:一个很有意思的概念,让并行操作某一数据的线程由并行变成串行。
那么这里锁使用人能力的体现在哪呢?锁的粒度(锁的范围)和锁的合理性
使用:其实就是一个通过某个关键字进行修饰了一个代码块或者是一个方法,在调用的时候是用过线程调用,凡是调用到该方法的线程,根据特定是先排队还是先去尝试获取锁,反正就是同一时刻只能有一个线程在操作(读锁除外)。
线程的创建上述边的文章中有:
线程的创建和使用
码文字-可以忽略
锁的粒度 (Granularity of Locks):(锁的范围)
1、锁分为粗粒度锁和细粒度锁。
粗粒度锁意味着锁的范围或者影响的区域较大,比如对整个集合对象加锁。
细粒度锁则是对较小范围的加锁,比如只对集合中的单个元素或者一部分元素加锁。
2、细粒度锁可以提高并发度,因为它允许多个线程同时操作不同部分的资源。
3、粗粒度锁编程简单,但可能降低并发度,因为即使是访问不同资源的操作也必须等待锁的释放。设计中需要平衡锁的粒度,以达到并发和性能上的最佳效果。
锁的合理性 (Appropriateness of Locks):
1、什么位置使用锁,以确保程序是线程安全的而不引起死锁或资源争用过高的情况。
2、锁应该保护共享资源,并且只在修改这些资源时才持有锁。
如果使用不合理,比如不必要的长时间持有锁或在不需要的时候使用锁,会导致性能问题或死锁。
合理使用锁还包括选择适当类型的锁,如可重入锁、读/写锁还是自旋锁等。
java中的锁-上正文
提示:这里可以添加技术整体架构
1、内置锁synchronized
底层原理:内置锁是基于JVM实现的同步机制。使用monitorenter和monitorexit两个指令来支持同步。对象在被线程访问时会加锁,同时每个对象维护一个监视器锁(Monitor),来保证只有获得该锁的线程可以执行同步代码块。
性能:历史上,synchronized被认为性能较差,但随后的JVM优化如偏向锁、轻量级锁、自旋锁等使其性能大幅提升。
如何使用:
public class SynchronizedExample {
// 对象锁示例,方法锁
public synchronized void synchronizedMethod() {
// 临界区,同一时刻只能有一个线程执行这里的代码
}
// 类锁示例,锁定的是这个类的所有实例
public static synchronized void synchronizedStaticMethod() {
// 临界区
}
// 代码块锁示例
public void synchronizedBlock() {
Object lock = new Object();
synchronized (lock) {
// 临界区
}
}
}
synchronized 的底层原理补充::
监视器锁(Monitor Lock):
synchronized 关键字在底层依赖于监视器锁(或 intrinsic lock)。
在 JVM 中,每一个被 synchronized 修饰的对象都与一个监视器锁相关联。
当线程进入 synchronized 同步块时,它会自动获取监视器锁;当离开同步块时(无论是由于正常执行流程还是由于异常退出),它会自动释放锁。
锁优化:
JVM 会对 synchronized 进行一系列优化,比如无锁、偏向锁、轻量级锁、重量级锁。
偏向锁假设最后一个获得锁的线程会再次获取锁,减少了锁操作的开销。
轻量级锁通过在对象头上的CAS操作尝试获取锁。
如果有多个线程竞争相同的锁(轻量级锁失败时),那么锁会膨胀为重量级锁,
此时 JVM 会在操作系统层面创建一个互斥量(mutex)。
重量级锁:
如果有多个线程同时试图获取相同的 synchronized 锁,则这些线程会被放入锁的等待队列中。当锁被释放时,队列中的线程会被唤醒(可能在Java虚拟机协助下通过操作系统实现)来尝试获取锁。
重量级的 synchronized 锁操作涉及操作系统级别的互斥量,这可能导致线程从用户态切换到内核态,这一过程是昂贵的,因为它涉及到相对重的上下文切换开销。
2、可重入锁 (ReentrantLock)
底层原理:ReentrantLock是从Java SE 5开始提供的一种显式锁,在java.util.concurrent.locks包下。它提供了一些synchronized没有的特性,如:等待可中断、公平锁、锁绑定多个条件。
性能:通常ReentrantLock的性能和synchronized相近,但在高度争用的情况下ReentrantLock可能表现更好。此外,ReentrantLock提供了更多的灵活性,可能会带来更好的性能。
如何使用:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock(); // 获取锁
try {
// 临界区
} finally {
lock.unlock(); // 释放锁,放在finally块中确保释放
}
}
}
ReentrantLock 的底层原理补充:
API 和操作:
ReentrantLock 提供了一个基于 API 的锁机制,它实现了 Lock 接口,并提供手动获取释放锁的方法。ReentrantLock 也是重入的,意味着它允许线程多次获取已经持有的锁。
队列同步器(AbstractQueuedSynchronizer,AQS):
ReentrantLock 底层使用了 AQS 提供的框架。AQS 使用一个内部的 FIFO 队列来管理线程的获取和释放锁。
AQS 通过一个叫做 state 的原子变量来控制锁的状态,它利用 CAS 操作来修改这个状态,比如进行独占式获取和释放。
公平锁和非公平锁:
ReentrantLock 可以创建为公平锁或非公平锁。公平锁通过维护一个有序队列来保证“先来先服务”的锁分配,而非公平锁则可能允许请求锁的新线程插到队列前面。
锁获取和释放:
当线程尝试获取一个已被其他线程持有的锁时,它可能会被挂起,并加入到 AQS 维护的等待队列中。
ReentrantLock 同样在内部使用了操作系统层面的互斥量(当线程阻塞时),这意味着 AQS 也可能导致用户态与内核态之间的上下文切换。
用户态与内核态的调度:
在 Java 中,由于 VM 管理着线程,synchronized 和 ReentrantLock 的轻量级锁实现主要在用户态完成。
但当存在线程阻塞,或者竞争状态下的锁需要操作系统介入时,JVM 需要与操作系统的调度器通信,这就涉及到用户态与内核态之间的切换。
用户态(User Mode)运行的是用户进程代码,
内核态(Kernel Mode)运行的是操作系统的代码。
线程在用户态时系统调用代价较低,在内核态时系统调用开销较大,因为它涉及到上下文切换,保存和恢复上下文信息等。
在线程争用非常激烈导致锁升级为重量级锁时,线程在获取或释放锁的过程中可能会由用户态进入内核态,此时使用的是操作系统提供的同步原语(如互斥量、信号量等),由操作系统的调度器来管理线程的睡眠和唤醒。
总的来说,synchronized 和 ReentrantLock 在轻量级操作时多在用户态处理,
避免了内核态的上下文切换开销,
但当锁竞争激烈时必须涉及内核态,以便合理安排线程的执行顺序。
在 JDK 6 之后的版本中,对 synchronized 的优化已经使其性能相比以前有很大提高,
让 synchronized 和 ReentrantLock 的性能不再有显著差异。
在某些复杂的景景下(例如需要条件变量、可中断获取锁、尝试非阻塞获取锁时),ReentrantLock 提供了更为灵活的高级功能。
、
3、读写锁 (ReadWriteLock)
底层原理:ReadWriteLock提供了一对锁,一个读锁和一个写锁。通过分离读和写操作,它允许多个读线程同时访问,但只允许一个写线程。
性能:在读多写少的情况下,ReadWriteLock可以提高并发性能。
如何使用:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读操作使用读锁
public void readOperation() {
readWriteLock.readLock().lock();
try {
// 临界区,可以有多个读操作同时执行
} finally {
readWriteLock.readLock().unlock();
}
}
// 写操作使用写锁
public void writeOperation() {
readWriteLock.writeLock().lock();
try {
// 临界区,同时只能有一个写操作
} finally {
readWriteLock.writeLock().unlock();
}
}
}
4、StampedLock
底层原理:StampedLock是Java SE 8引入的,也在java.util.concurrent.locks包下。它是一种提供了三种访问模式的锁,包括写锁、悲观读锁与乐观读(tryOptimisticRead)。
性能:StampedLock的性能通常优于ReadWriteLock,因为它的读锁机制更加灵活。但是StampedLock不是可重入锁。
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock stampedLock = new StampedLock();
// 写锁示例
public void write() {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
// 临界区
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
// 悲观读锁示例
public void read() {
long stamp = stampedLock.readLock(); // 获取悲观读锁
try {
// 临界区
} finally {
stampedLock.unlockRead(stamp); // 释放读锁
}
}
// 乐观读锁示例
public void optimisticRead() {
long stamp = stampedLock.tryOptimisticRead(); // 试图获取乐观读锁
// 临界区代码
if (!stampedLock.validate(stamp)) { // 检查stamp是否在读期间被修改过
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
// 重试临界区代码 (此处可以保证数据的一致性)
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
}
}
5、自旋锁 (Spin Lock)
自旋锁并不直接作为API存在于Java中,但可以通过原子变量类自行实现。自旋锁的特点是线程在获取锁时不会立即阻塞,而是在循环中反复检查锁是否可用。
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
while (!owner.compareAndSet(null, currentThread)) {
// 循环等待
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
owner.compareAndSet(currentThread, null);
}
}
6. 条件变量 (Condition)
Condition提供一种更加灵活的线程协调机制相比于Object类中的wait、notify和notifyAll方法。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
// 等待信号
condition.await();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
// 发送信号
condition.signal();
} finally {
lock.unlock();
}
}
}
7. 信号量 (Semaphore)
信号量用于控制同时访问特定资源的操作数量,它可以用于实现资源池或给代码区域加并发限制。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(10); // 允许10个线程同时访问
public void accessResource() throws InterruptedException {
semaphore.acquire(); // 获取一个许可
try {
// 访问受限资源
} finally {
semaphore.release(); // 释放一个许可
}
}
}
8. 倒数锁 (CountDownLatch)
CountDownLatch使一个线程可以等待直到倒计数结束,然后开始执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(3);
public void worker() throws InterruptedException {
// 执行任务
latch.countDown(); // 工作完成,计数器减1
}
public void waiter() throws InterruptedException {
latch.await(); // 等待直到计数器为0
// 执行后续操作
}
}
补充:volatile
Java 语言中的一个关键字,它用于将变量声明为“易变的”。当一个变量被声明为 volatile 后,这条声明向编译器与运行时表明,这个变量可能会被某些未知的因素(如其他线程)意外修改。其主要作用是确保变量的读写操作对所有线程都是可见的,也就是说,一个线程内对 volatile 变量的读或写将立刻反映到其他线程中。
使用 volatile 关键字通常比锁更轻量级,因为 volatile 变量的操作不会导致线程的阻塞。但是请注意,volatile 只能保证单个变量读写的原子性和内存可见性,并不能保证复合操作的原子性。
public class VolatileExample {
private volatile int counter = 0; // 将变量声明为 volatile
public void increment() {
counter++; // 注意:这不是原子操作
}
public int getCounter() {
return counter; // 读取操作的内存可见性由 volatile 关键字保证
}
}
需要注意,counter++ 不是一个原子操作,它实际上是分为三步执行的:读取 counter 的值,将值加一,然后写回新值。因此,在多线程环境中,如果没有其他同步措施,这个操作仍然不是线程安全的。
volatile 的内部原理:
内存可见性:
在 Java 内存模型中,每个线程都有自己的工作内存,它包含了主内存中所有可见变量的副本。当变量被声明为 volatile 后,线程将直接在主内存中进行读写操作,不会在工作内存中缓存该变量。
防止指令重排序:
Java 编译器和处理器都可能会对操作进行重排序,以优化性能。然而,在某些场景下,这种重排序可能会打破程序的逻辑。volatile 关键字可以用来告诉 JVM 不允许对该变量相关的读写操作进行重排序。
示例使用 volatile 的情况:
示例使用 volatile 的情况:
public class VolatileFlagExample {
private volatile boolean flag = true; // 一个简单的状态标志
public void runTask() {
while (flag) {
// 执行任务
}
}
public void stopTask() {
flag = false; // 改变这个标志的值来停止任务
}
}
在这个例子中,volatile 保证了 flag 变量的改变对所有线程立即可见,使得在 runTask 方法内部循环的线程能够看到标志值的更新,并且能够安全地退出循环。
在选择使用 volatile 还是锁时,需要根据你的具体需求做出决定。如果只需要保证单个变量的内存可见性,使用 volatile 通常是更合适的。如果需要保证复合操作或多个变量的同步,你可能需要使用锁来保证原子性。
概念
提示:纯纯的概念,尝试理解就行了
并发编程三大特性
1、原子性 (Atomicity)
原子性指的是在一系列操作中,要么所有的操作全部完成,要么全部不起作用,不会出现部分完成的情形。在多线程环境中,原子性确保了当多个线程尝试同时更新同一个变量时,每次只有一个操作能够在不被其他操作中断的情况下执行。
在Java中,对基本数据类型的变量(除了long和double之外的类型)的单个读或写操作是原子性的,但是对于复合操作(比如count++)则不是,因为这包含了三个独立的操作:读取变量值、增加变量值,并且写回新值。为了实现复合操作的原子性,可以使用synchronized关键字或java.util.concurrent.atomic包下提供的原子类(如AtomicInteger)。
2、可见性 (Visibility)
可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。在多线程环境下,由于各线程可能在自己的工作内存中缓存变量的副本,所以一个线程对一个共享变量的修改可能不会立即反映到其他线程上,这就会导致所谓的可见性问题。
Java提供了volatile关键字来保证变量的可见性。当一个变量被声明为volatile后,所有对它的读写都将直接进行在主存上,这保证了每次读取变量都能得到最新的值。
3、指令重排序 (Instruction Reordering)
指令重排序是编译器和处理器为了优化程序性能而对指令序列进行重新组织的一种手段。在没有依赖性的情况下,编译器和处理器可能会改变代码的执行顺序。
指令重排序可能会导致在多线程程序中出现意想不到的结果,因为程序的实际执行顺序可能与代码书写顺序不一致。例如,编译器可能会重排序一些语句以提升性能,但这可能会破坏线程之间对共享数据访问的假设。
为了防止指令重排序对多线程程序的影响,Java内存模型提供了happens-before原则来确保在特定的操作序列上,一些关键的内存写入操作对其他线程是可见的。此外,Java的volatile关键字同样会防止变量读写的指令重排序。
在编写并发程序时,理解这三个概念是至关重要的,它们也是保证多线程程序正确性的基石。开发人员必须正确使用同步技术和并发工具来确保程序具有期望的原子性、可见性和防止不必要的指令重排序。
锁的本质
锁的本质是一种同步机制,用于控制多个线程对共享资源的访问,以防止并发时出现的问题,如数据的不一致性和竞态条件。锁通常通过以下机制来实现这一点:
互斥(Mutual Exclusion):
锁保证了在某一时刻,只有一个线程可以拥有对共享资源的独占访问权限。这是通过阻塞其他试图访问该资源的线程来实现的,直到拥有锁的线程释放了锁。
状态同步:
锁不仅仅用于互斥,还可以用来同步一个线程对共享资源状态的改变让其他线程看到,即在锁定和解锁时确保内存屏障的设置,使得之前的写动作对后续的读动作可见。
可重入性:
许多锁有可重入性的特点(如ReentrantLock和synchronized关键字),意味着同一个线程可以多次获取同一把锁。这保证了在一个已经持有锁的线程调用另一个使用相同锁保护的同步方法时不会出现死锁。
线程调度:
锁的实现通常涉及到某种形式的线程调度。锁需要管理哪些线程正在等待锁,以及在锁被释放时哪个线程应该获得该锁。
所有权:
锁模型涉及到锁的所有权概念,即只有持有锁的线程才能解锁。
公平性:
一些锁实现(尤其是那些显式锁)可能提供选择公平性的选项。
公平锁按照线程请求锁的顺序来授予锁,而非公平锁则可能允许"插队"。
锁的具体实现方式可以涉及到底层操作系统的原语,比如在POSIX线程(pthreads)中的互斥量(mutexes)和条件变量(condition variables),以及Java线程模型中的wait/notify机制和内置与显式锁等。
此外,底层的锁实现也依赖于硬件指令,如处理器提供的Test-and-Set,Compare-and-Swap(CAS),Load-Linked/Store-Conditional等,这些原子操作可以帮助实现同步原语。而现代JVM为了提高性能,还会使用一系列的锁优化技术,如偏向锁定、轻量级锁定和锁粗化。
总的来说,锁的本质是一种协调机制,它为控制并发访问共享资源提供了一种方法,让编程模型可以在多线程环境中维护稳定和正确性。
常见问题:
1、死锁(Deadlock):
当多个线程彼此等待对方持有的锁时,可能发生死锁。在这种情况下,涉及的线程都无法继续执行,因为它们各自等待的条件永远无法满足。
2、活锁(Livelock):
活锁是一个线程尽管没有被阻塞(即不在等待状态),却仍然无法向前执行,因为它不断重试一个总是失败的操作。活锁通常发生在两个线程试图通过持续响应对方的行为来解决冲突时。
3、饥饿(Starvation):
当一个或多个线程无限期地被阻止执行(通常是未能获取必要的锁),因为其他线程持续占用资源或者由于线程调度器优先选择其他线程时,就会发生饥饿。在公平锁的设计下饥饿的可能性降低,但在非公平锁的情况下更常见。
4、优先级反转(Priority Inversion):
一个低优先级的线程持有一个高优先级的线程需要的锁,而低优先级的线程由于某种原因(如长时间操作或被更高优先级的线程打断)未能及时释放锁,这就可能导致高优先级线程长时间等待,从而降低了系统性能。
5、上下文切换开销(Context Switch Overhead):
在大量线程竞争少量的锁时,会频繁发生上下文切换,因为线程在尝试获取锁时会被阻塞。上下文切换可能会带来显著的性能损耗。
6、性能瓶颈(Performance Bottlenecks):
过度的锁竞争会导致性能瓶颈,特别是当锁保护的是一个高度争用的资源时,这可能限制应用程序的伸缩性和吞吐量。
7、锁颗粒度不当(Improper Lock Granularity):
锁的颗粒度过粗或过细都可能导致问题。颗粒度过粗的锁会降低并发度,而颗粒度过细的锁可能会增加复杂性和潜在的死锁机会。
8、忘记释放锁(Forgetting to Release Locks):
忘记在代码的某个分支中释放锁将导致其他线程无法再获得该锁。
9、不适当的锁类型选择(Choosing the Wrong Type of Lock):
锁的选择应该依据资源的访问模式来决定。错误的选择比如在读多写少的场景中使用排他锁而不是读写锁,可能导致性能不理想。
10、嵌套锁(Nested Locks):
在使用多个锁时,如果不按照一致的顺序获取锁,可能增加死锁的风险。
11、无限期等待(Indefinite Waiting):
一些锁可能导致线程无限期地等待获取锁,这种情况下引入超时或者在等待时采取其他操作可能是一种改进策略。
为避免这些问题,需要谨慎设计锁策略,精心选择适合的锁类型,编写安全的同步代码,合理利用并发工具,并在编写和测试阶段严格验证线程安全性。此外,锁也可以配合其他并发控制技术,如原子变量、并发集合和执行框架等来构建健壮的多线程应用。
至于怎么解决,其实多半都是编程的问题,对于同一个业务中的资源,尽量避开使用多个锁来实现,这样很容易造成死锁
那么关于上述的其他问题,那测试一下就能解决。当业务系统很复杂的时候一定要确定锁的粒度,对数据的修改源做收口,尽量保证业务可控。不要有接口逃逸