【并发编程】4.JUC中常用的锁
JUC即java.util.concurrent的简称,在这个包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架,还提供了设计用于多线程上下文中。通过她们能够很好地帮助我们在开发中提高一些程序的性能。
1.Lock与Condition
- condition
Lock与condition是Java中管程模型除了synchronized的另一套实现,不同是的支持多条件队列。Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll()。
synchronized的管程模型
Lock&Condition的管程模型
Condition的使用 需要获取到锁
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
上述代码中就是有两个条件变量,对应两个条件队列,分别进行入队和出队。 队列已满通知等待出队操作的线程,队列已空则通知等待入队操作的线程。
如果是使用synchronized关键字实现的话调用notify/notifyAll方法应该是通知唯一的等待队列里的所有线程,然后判断是执行入队或者是出队。
- ReentrantLock
上述代码中使用的锁就是 ReentrantLock 即为可重入锁,就是再已经获取锁的情况下可以再次获取到同一把锁。
class X {
private final Lock rtl = new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock();
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); //get方法再次获取锁
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
- ReadWriteLock
读写锁,适用于读多写少的场景,例如缓存
ReadWriteLock 是接口
ReentrantReadWriteLock 是实现类
- 允许多个线程同时读共享变量; (优于互斥锁的关键) 如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量 (区别于对读操作不加锁)
/**
* className: Cache
* create by: zhujun
* description: 使用读写锁实现的缓存工具类
* 读写锁 使用于读多写少的并发场景
* create time: 2019/7/23 11:21
*/
public class Cache<K,V> {
//hashmap 存储数据
final HashMap<K,V> hashMap = new HashMap<>();
final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
final Lock readLock = reentrantReadWriteLock.readLock();
final Lock writeLock = reentrantReadWriteLock.writeLock();
/**
* 存入数据
* @param key
* @param value
*/
void set(K key,V value){
writeLock.lock();
try{
hashMap.put(key,value);
}finally {
writeLock.unlock();
}
}
/**
* 读取数据
* @param key
* @return
*/
V get(K key){
//读锁
readLock.lock();
try{
return hashMap.get(key);
}finally {
readLock.unlock();
}
}
}
注意点:1.写锁支持条件变量,读锁不支持条件变量
2.支持锁的降级,不支持锁的升级
锁的升级
read.lock();
try {
v = m.get(key);//验证值是否存在
//获取写锁从数据库中更新缓存 //锁的升级
} finally{
read.unlock();
}
锁的降级
writeLock.lock();
try{
....
readLock.lock();//释放写锁之前 释放读锁
}finally{
writeLock.unlock();
readLock.lock()
}
- StampedLock
Java 1.8提供,性能优于ReadWriteLock 支持三种锁模式:写锁,乐观读锁,悲观读锁。
因为 ReadWriteLock 是悲观读锁,读取的时候不允许写入,StampedLock为了提高性能提供了乐观读锁,读的过程中大概率不会有写入。
首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。
接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。
如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。
/**
* className: Point
* create by: zhujun
* description:StampLock 的乐观读与悲观读锁
* create time: 2019/7/30 17:01
*/
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
final StampedLock s1 = new StampedLock();
//计算带到原点的举例
double getDistance() throws InterruptedException {
long stamp = s1.tryOptimisticRead();//乐观读
System.out.println("乐观读stamp:"+stamp);
int curX = x;
int cutY = y;
System.out.println("读取成功:"+x+","+y);
Thread.sleep(1000);//睡眠 方便测试时进行写操作
if(!s1.validate(stamp)){//通过验证stamp
//期间有写操作 升级为悲观读锁 等待写操作完成
stamp = s1.readLock();
try{
System.out.println("存在写操作,重新读取x,y坐标");
System.out.println("此时stamp:"+stamp);
curX = x;
cutY = y;
}finally {
s1.unlockRead(stamp);
}
}
return Math.sqrt(curX*curX+cutY*cutY);
}
void reLocation(int x,int y){
//写操作 写锁
long stamp = s1.writeLock();
System.out.println("写锁stamp:"+stamp);
try{
this.x = x;
this.y = y;
System.out.println("重定位成功:"+x+","+y);
}finally {
s1.unlockWrite(stamp);
}
}
}
public class PointTest {
public static void main(String[] args) throws InterruptedException {
Point p = new Point(1,2);
//线程1 计算距离
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
try {
double distance = p.getInstance();
System.out.println("距离原点的举例:"+distance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//线程2 重写坐标
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
p.reLocation(3,4);
}
});
th1.start();
th2.start();
}
}
注意:1.StampedLock 不支持可重入
2.如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁 writeLockInterruptibly()。