07_Java中的锁
文章目录
- 一、java中的锁
- 1. synchronized关键字
- 2. ReentrantLock
- 3. ReadWriteLock
- 4. StampedLock
- 5. Semaphore(信号量)
- 二、重点解析
- 1. synchronized修饰静态方法与普通方法的区别
- synchronized修饰普通方法
- synchronized修饰静态方法
- 总结
- 2. 公平锁和非公平锁的区别及实现机制
- 2.1 公平锁与非公平锁的区别
- 2.2 实现机制
- 2.3 CAS操作
- 3. 乐观锁与悲观锁
- 一、乐观锁原理
- 二、悲观锁原理
- 三、总结
一、java中的锁
以下是一些Java中常见的锁及其使用示例:
1. synchronized关键字
synchronized
是Java内置的一种锁机制,可以用于方法或代码块。
示例:synchronized方法
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;}public static void main(String[] args) {SynchronizedExample example = new SynchronizedExample();// 多个线程同时调用increment方法,会被同步锁保护}
}
示例:synchronized代码块
public class SynchronizedBlockExample {private final Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) {count++;}}public int getCount() {return count;}
}
2. ReentrantLock
ReentrantLock
提供了比synchronized
更灵活的锁机制。
示例:ReentrantLock
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {return count;}
}
3. ReadWriteLock
ReadWriteLock
允许多个读线程同时访问资源,但写线程在访问资源时会独占锁。
示例:ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private int count = 0;public void increment() {rwLock.writeLock().lock();try {count++;} finally {rwLock.writeLock().unlock();}}public int getCount() {rwLock.readLock().lock();try {return count;} finally {rwLock.readLock().unlock();}}
}
4. StampedLock
StampedLock
提供了三种锁模式:写锁、读锁和乐观读锁。
示例:StampedLock
import java.util.concurrent.locks.StampedLock;public class StampedLockExample {private final StampedLock stampedLock = new StampedLock();private int count = 0;public void increment() {long stamp = stampedLock.writeLock();try {count++;} finally {stampedLock.unlockWrite(stamp);}}public int getCount() {long stamp = stampedLock.tryOptimisticRead();int currentCount = count;if (!stampedLock.validate(stamp)) {stamp = stampedLock.readLock();try {currentCount = count;} finally {stampedLock.unlockRead(stamp);}}return currentCount;}
}
5. Semaphore(信号量)
Semaphore
允许你指定一个许可数量,只有持有许可的线程才能访问资源。
示例:Semaphore
import java.util.concurrent.Semaphore;public class SemaphoreExample {private final Semaphore semaphore = new Semaphore(3); // 允许3个线程同时访问public void accessResource() {try {semaphore.acquire(); // 获取许可// 访问共享资源的代码} catch (InterruptedException e) {e.printStackTrace();} finally {semaphore.release(); // 释放许可}}
}
这些示例展示了Java中不同的锁机制及其基本用法。在实际应用中,你需要根据具体的场景和需求选择合适的锁机制。
二、重点解析
1. synchronized修饰静态方法与普通方法的区别
[在Java中,synchronized
关键字可以用于修饰方法,以控制对共享资源的并发访问。当synchronized
修饰静态方法和普通方法时,它们之间存在一些关键的区别。下面我将详细解释这些区别:
synchronized修饰普通方法
当synchronized
修饰一个普通方法时,锁的是调用该方法的对象实例。这意味着,同一个类的不同对象实例之间的同步方法是互不干扰的。换句话说,一个线程可以访问一个对象实例的同步方法,而另一个线程可以同时访问同一个类的另一个对象实例的同步方法。
代码示例:
public class SynchronizedInstanceMethod {public synchronized void doSomething() {// 同步代码块}
}// 使用示例
SynchronizedInstanceMethod obj1 = new SynchronizedInstanceMethod();
SynchronizedInstanceMethod obj2 = new SynchronizedInstanceMethod();// 线程1可以访问obj1的doSomething方法
// 线程2可以同时访问obj2的doSomething方法,因为锁的是不同的对象实例
synchronized修饰静态方法
当synchronized
修饰一个静态方法时,锁的是调用该方法的类的Class
对象。这意味着,对于同一个类的所有对象实例,它们的静态同步方法是互斥的。换句话说,一个线程访问某个类的静态同步方法时,其他线程不能访问同一个类的任何其他静态同步方法(但可以访问该类的非静态同步方法,因为非静态同步方法锁的是对象实例)。
代码示例:
public class SynchronizedStaticMethod {public static synchronized void doSomethingStatic() {// 同步代码块}public synchronized void doSomethingInstance() {// 同步代码块}
}// 使用示例
SynchronizedStaticMethod obj1 = new SynchronizedStaticMethod();
SynchronizedStaticMethod obj2 = new SynchronizedStaticMethod();// 线程1访问doSomethingStatic方法时
// 线程2不能同时访问doSomethingStatic方法(因为锁的是类对象)
// 但线程2可以访问obj1或obj2的doSomethingInstance方法(因为锁的是对象实例)
总结
synchronized
修饰普通方法时,锁的是对象实例。synchronized
修饰静态方法时,锁的是类的Class
对象。
理解这些区别对于正确设计多线程程序至关重要,因为它直接影响到程序的并发行为和性能。希望这能帮助你更好地理解synchronized
关键字在Java中的使用!
2. 公平锁和非公平锁的区别及实现机制
java架构师{type=“agent” src=“https://lingjing-online.cdn.bcebos.com/v1/lingjing-online/agent_logo/2025-03-19/69dbf7f5-8a72-4520-bce3-743773826ed2.JPEG?x-bce-process=image/format,f_webp”} 在Java并发编程中,锁是用于控制多个线程对共享资源访问的重要机制。锁有两种常见的策略:公平锁(Fair Lock)和非公平锁(Non-Fair Lock)。下面我将详细介绍这两种锁的区别及实现机制。
2.1 公平锁与非公平锁的区别
-
获取锁的顺序:
- 公平锁:多个线程在获取锁时,按照先后顺序来获取,遵循“先到先得”的原则。也就是说,当多个线程请求同一个锁时,锁会按照线程请求的顺序进行分配,等待时间最长的线程将优先获取锁。
- 非公平锁:不保证线程按照请求锁的顺序来获取锁。每个线程在请求锁时,都会直接尝试获取锁,如果锁可用则立即获取,不能获取时再进入队列排队。这种方式更加“激进”,允许后来的线程可能会比之前的线程先获得锁。
-
性能:
- 公平锁:由于公平锁需要维护一个严格的等待队列,并在每次获取锁时检查队列,判断当前线程是否在队列的前端,这增加了锁获取的开销,导致性能相对较低。
- 非公平锁:由于非公平锁不需要维护一个严格的等待队列,线程可以直接尝试获取锁,减少了锁竞争和上下文切换的开销,因此在高并发场景下性能通常优于公平锁。
-
线程饥饿问题:
- 公平锁:通过按顺序分配锁,确保每个线程最终都会获得锁,不会导致某些线程一直处于等待状态,从而避免线程饥饿现象。
- 非公平锁:由于非公平锁允许后来的线程插队,某些线程可能长时间无法获取锁,导致线程饥饿问题。
-
适用场景:
- 公平锁:适合在对线程公平性要求较高的场合,如银行转账、物流系统等需要按顺序处理的业务场景。
- 非公平锁:因其高性能在大多数场景下被优先采用,尤其是对性能要求较高的应用场景。
2.2 实现机制
-
公平锁的实现:
- 在Java中,
ReentrantLock
可以通过构造函数设置为公平锁,例如:ReentrantLock lock = new ReentrantLock(true);
(其中true
表示公平锁)。 - 当线程请求锁时,公平锁会将这些请求放入一个队列中(如
AbstractQueuedSynchronizer
中的队列),按照队列的顺序依次为线程分配锁。
- 在Java中,
-
非公平锁的实现:
- 在Java中,
ReentrantLock
默认是非公平锁,例如:ReentrantLock lock = new ReentrantLock();
。 - 当线程请求锁时,非公平锁会直接尝试抢占锁,如果锁没有被其他线程持有,当前线程会立即获取锁。
- 非公平锁的实现通常依赖于CAS(Compare-And-Swap)操作或者通过一个简单的循环尝试来获取锁。
- 在Java中,
综上所述,公平锁和非公平锁在获取锁的顺序、性能、线程饥饿问题以及适用场景方面存在显著差异。在实际开发中,应根据具体需求选择合适的锁策略。
2.3 CAS操作
在CAS(Compare-And-Swap)操作中,确实会涉及三个关键的操作数:内存位置V、预期原值A和新值B。操作的流程是这样的:
比较:首先,会比较内存位置V当前存储的值与预期原值A。
交换:如果这两个值相等,说明没有其他线程在这段时间内修改过这个值,那么就将内存位置V的值更新为新值B。
不做操作:如果这两个值不相等,说明有其他线程已经修改过这个值,那么当前线程就不会进行任何操作,通常会重新读取内存位置V的当前值,并可能再次尝试CAS操作,或者采取其他同步机制。
这个机制的关键在于它的原子性,即比较和交换这两个步骤是不可分割的,要么全部执行,要么都不执行,这样就不会出现线程安全问题。
3. 乐观锁与悲观锁
在Java中,乐观锁与悲观锁是两种常见的并发控制策略,它们各自有着不同的原理和应用场景。下面我将对这两种锁的原理进行详细讲解。
一、乐观锁原理
乐观锁的核心思想是“先操作,再检查”。它假设在大多数情况下,多个线程之间不会发生冲突,因此不会主动加锁。在更新数据时,乐观锁会使用一种“检测机制”来判断是否真的发生了冲突。
- 版本号机制:这是乐观锁常用的一种实现方式。在数据库中,可以为每个数据行添加一个版本号字段。当线程读取数据时,会同时读取该数据的版本号。在更新数据时,会比较当前数据的版本号与读取时的版本号是否一致。如果一致,说明没有其他线程修改过数据,可以安全地提交更新;如果不一致,说明数据已被其他线程修改,此时会进行冲突处理,通常是重新读取数据并尝试更新。
- CAS操作:在Java中,乐观锁还可以通过CAS(Compare-And-Swap,比较并交换)操作来实现。CAS是一种无锁的操作,它会比较当前值和期望值是否一致,如果一致就更新新值,否则就失败并重试。这种方式适用于对变量进行原子更新的场景。
乐观锁适用于读操作频繁而写操作较少的场景,可以减少锁的使用,提高并发性能。同时,由于乐观锁不会主动加锁,因此不会出现死锁问题。但是,如果并发冲突频率较高,可能会导致频繁的重试,反而降低性能。
二、悲观锁原理
与乐观锁相反,悲观锁的核心思想是“先加锁,再操作”。它假设在多线程环境下,对共享资源的访问会产生冲突,因此默认认为每次访问都会发生冲突,需要加锁来保证独占访问。
- 数据库锁机制:在数据库中,悲观锁通常通过SELECT … FOR UPDATE语句来实现。当一个线程执行该语句时,数据库会给查询的那条数据加锁,其他线程无法修改这条数据,直到锁被释放。这种方式适用于对数据库记录进行独占访问的场景。
- Java同步锁:在Java中,悲观锁可以通过synchronized关键字或ReentrantLock来实现。当一个线程进入synchronized修饰的方法或代码块时,或显式地获取ReentrantLock锁时,其他线程将被阻塞,直到该线程退出同步块或释放锁。这种方式适用于对共享变量进行同步访问的场景。
悲观锁能够完全避免并发冲突,因为加锁后其他线程无法访问数据。在冲突频率较高的场景下(如多个线程频繁修改数据的场景),悲观锁的性能更稳定。但是,由于加锁会导致其他线程等待,因此悲观锁可能会降低系统吞吐量。同时,如果锁没有正确释放,可能会导致死锁问题。
三、总结
乐观锁和悲观锁各有优缺点,适用于不同的场景。在选择使用哪种锁时,需要根据具体的业务需求和并发情况来进行权衡。如果读操作频繁而写操作较少,且对并发性能要求较高,可以选择乐观锁;如果写操作频繁或并发冲突较多,需要保证数据的一致性和线程安全性,可以选择悲观锁。