一. Lock接口下的实现类
在Java中,Lock
接口是 java.util.concurrent.locks
包中的一部分,它提供了比 synchronized
更丰富的锁操作。Lock
接口的实现类包括 ReentrantLock
(可重入锁)、ReadWriteLock
(读写锁) 等。
1. ReentrantLock可重入锁
ReentrantLock
是 Java 中 java.util.concurrent.locks
包下的一个可重入锁实现。可重入锁意味着一个线程可以多次获得同一把锁,而不会产生死锁。
1.1. 可重入锁的原理
- 线程安全性:确保只有一个线程可以同时访问锁保护的资源。
- 可重入性:同一个线程可以多次获得同一把锁,每获得一次锁,锁的持有计数器就增加一次,只有当锁的持有计数器为零时,其他线程才有机会获得这把锁。
1.2. ReentrantLock的特点
- 公平性选择:可以选择公平锁或非公平锁。公平锁按照线程请求锁的顺序来分配锁,非公平锁则可能让等待时间较短的线程优先获得锁。
- 条件变量支持:提供了与锁相关联的条件变量,用于实现等待/通知机制。
- 中断处理:支持锁的中断获取,可以在获取锁的过程中响应中断。
1.3. 使用注意
- 锁的释放:确保在
finally
块中释放锁,以避免因异常导致锁无法释放。- 避免死锁:即使
ReentrantLock
是可重入的,也需要小心避免死锁,例如,确保获取锁的顺序一致。- 性能考虑:锁操作有一定的性能开销,需要根据实际情况权衡使用锁的范围和粒度。
- 默认创建非公平锁:如果传入true则创建的是公平锁 ReentrantLock lock = new ReentrantLock(true);
1.4. 代码示例
- 测试代码
package com.demo.jucdemo;
import java.util.concurrent.locks.ReentrantLock;
/**
* 文件名:ReentrantLockExample
* 创建者:
* 创建时间:2024-09-21
* 描述:模拟银行 存款,取款操作
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
//初始账户为余额为0
AccuntInfoRL accuntInfoRL = new AccuntInfoRL("小明",0);
//循环启动线程
for (int i = 0; i < 10; i++) {
//模运算来判断是存款还是取款
if (i % 2 == 0) {
new Thread(() -> {
accuntInfoRL.save(10);
}, "存款").start();
} else {
new Thread(() -> {
accuntInfoRL.withdrawal(10);
}, "取款").start();
}
}
}
}
/**
* 模拟测试用户
* 提供两个方法,分别是取款,存款
*/
class AccuntInfoRL{
private String name;
private int asset;
AccuntInfoRL(String name,int asset) {
this.name = name;
this.asset = asset;
}
/**
* 创建可重入锁时,默认是非公平锁
* new ReentrantLock(true) 传入ture时创建的是公平锁
*/
ReentrantLock lock = new ReentrantLock();
/**
* 存款方法
* @param asset
*/
public void save(int asset) {
//1.加锁
lock.lock();
try {
//2.业务代码
this.asset += asset;
System.out.println(this.name+"-存款:"+asset +" || 余额:"+this.asset);
} catch (Exception e) {
throw new RuntimeException("存款失败");
}finally {
//3.释放锁
lock.unlock();
}
}
/**
* 取款方法
*/
public void withdrawal(int asset){
//1.加锁
lock.lock();
try {
//2.业务代码
//先判断金额是否足够
if(asset <= this.asset){
this.asset -= asset;
System.out.println(this.name+"-取款:"+asset+" || 余额:"+this.asset);
}else {
//如果余额不足直接打印,不做操作
System.out.println("余额不足,取款失败");
}
} catch (Exception e) {
throw new RuntimeException("取款失败");
}finally {
//3.释放锁
lock.unlock();
}
}
}
- 测试结果
2. ReentrantReadWriteLock读写锁
ReentrantReadWriteLock
是 Java 中实现读写锁的一个类,它支持一个读锁和一个写锁。读锁可以由多个线程同时持有,而写锁是排他的,一次只能由一个线程持有。这种锁非常适合读多写少的场景,因为它允许多个线程同时进行读操作,从而提高了程序的并发性能。
Lock 接口的读写锁是通过 ReentrantReadWriteLock 内部静态类实现的
ReentrantReadWriteLock.WriteLock.lock() // 写锁
ReentrantReadWriteLock.ReadLock.lock() // 读锁
2.1. 实现原理
锁的分离:
ReentrantReadWriteLock
将锁分为读锁和写锁,读锁可以由多个线程共享,写锁则是排他的。可重入:无论是读锁还是写锁,都支持可重入性,即同一个线程可以多次获取同一把锁。
锁状态:锁的状态由一个整型变量
state
表示,其中高16位表示读锁的计数,低16位表示写锁的计数。公平性:
ReentrantReadWriteLock
可以是公平的也可以是非公平的。公平锁保证线程获取锁的顺序是公平的,而非公平锁则可能让某些线程优先获取锁。锁的获取:
- 读锁获取:如果写锁没有被占用,线程可以获取读锁,并且读锁可以被多个线程共享。
- 写锁获取:如果读锁和写锁都没有被占用,线程可以获取写锁。
锁的释放:
- 读锁释放:释放读锁时,读锁计数减一,如果读锁计数为零,则可能允许等待的写锁获取锁。
- 写锁释放:释放写锁时,写锁计数减一,并且唤醒等待的读锁或写锁。
锁的降级:锁降级是允许的,即线程可以先释放写锁,再获取读锁。
锁的升级:锁升级是不允许的,即线程不能先获取读锁,再获取写锁,因为这可能导致死锁。
2.2. 注意事项
- 锁的公平性:公平锁可能会降低性能,但可以避免线程饥饿问题。
- 锁的升级:避免在持有读锁的情况下尝试获取写锁,因为这可能导致死锁。
- 锁的释放:确保在
finally
块中释放锁,以避免死锁。
2.3. 代码示例
- 创建银行账户类
package com.demo.jucdemo; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 文件名:AccountInfo * 创建者: * 创建时间:2024-08-22 * 描述: 银行账户信息,存款方法,取款方法,查询账户余额方法 */ public class AccountInfo { private String name; private int asset; AccountInfo(String name, int asset) { this.name = name; this.asset = asset; } //创建读写锁 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); /** * 存款方法,需要使用写锁 * @param asset */ public void save(int asset) { //1.获取写锁 this.reentrantReadWriteLock.writeLock().lock(); try { //2.业务代码 this.asset += asset; System.out.println(this.name+"-存款:"+asset +" || 余额:"+this.asset); } catch (Exception e) { throw new RuntimeException("存款失败"); }finally { //释放锁 this.reentrantReadWriteLock.writeLock().unlock(); } } /** * 查询账户余额方法 * 需要使用读锁, * @return */ public AccountInfo queryRemaining(){ //1.获取读锁 this.reentrantReadWriteLock.readLock().lock(); try { //2.业务处理 return new AccountInfo(this.name,this.asset); } catch (Exception e) { return null; }finally { //3.释放读锁 this.reentrantReadWriteLock.readLock().unlock(); } } /** * 取款方法 * 账户减钱使用写锁 */ public void withdrawal(int asset){ //1.获取写锁 this.reentrantReadWriteLock.writeLock().lock(); try { //2.业务处理 //先判断金额是否足够 if(asset <= this.asset){ this.asset -= asset; System.out.println(this.name+"-取款:"+asset+" || 余额:"+this.asset); }else { //如果余额不足直接打印,不做操作 System.out.println("余额不足,取款失败"); } } catch (Exception e) { throw new RuntimeException("取款失败"); }finally { //3.释放写锁 this.reentrantReadWriteLock.writeLock().unlock(); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAsset() { return asset; } public void setAsset(int asset) { this.asset = asset; } }
- 创建测试类
package com.demo.jucdemo; /** * 文件名:Main * 创建者: * 创建时间:2024-08-19 * 描述:测试类 */ public class Main { public static void main(String[] args) { //初始化账户信息,金额为0 AccountInfo account = new AccountInfo("小红", 0); //循环启动线程 for (int i = 0; i < 10; i++) { //1.开启个新线程查询余额 new Thread(() -> { //取款之后查询余额 AccountInfo accountInfo = account.queryRemaining(); System.out.println("查询账户余额:"+"账户名称:"+accountInfo.getName()+" || 账户余额:"+accountInfo.getAsset()); }, "查询余额").start(); //2.模运算来判断是存款还是取款 if (i % 2 == 0) { new Thread(() -> { account.save(10); }, "存款").start(); } else { new Thread(() -> { account.withdrawal(10); }, "取款").start(); } } } }
- 测试结果