一、读写锁基本特性
我们知道,对锁性能的优化其中有一条:如果操作互不影响,那么锁就可以被分离。这就是锁分离的思想。
ReentrantReadWriteLock可重入的读写锁。读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。
ReentrantReadWriteLock内部有两把锁,一把读锁,一把写锁。通过分离读锁和写锁,使得它比其它排他锁性能更好。
ReentrantReadWriteLock有下面三个特性:公平性、重进入、锁降级。
二、读写锁基本实现原理
ReentrantReadWriteLock 内部也是通过 队列同步器AQS 实现的(看来AQS是开启并发编程的关键突破口!)。
AQS 中的一个同步状态 state 表示当前共享资源是否被其他线程锁占用。如果为0则表示未被占用,其他值表示该锁被重入的次数。
如何才能在一个int类型的变量上,记录 读锁与写锁的 状态?
用 按位切割 将这个变量分为两部分,高16位记录读锁的状态,低16位记录写锁的状态
下图中,当前状态有一个线程已经获取了写锁,且重入了两次,同时也获取了两次读锁。
实现原理也很简单。
(1)如果写状态的值为0,读状态的值不为0,那么当前线程获取的就是读锁。
(2)如果读状态的值为0,写状态的值不为0,那么当前线程获取的就是写锁。
(3)如果读状态的值与写状态的值均不为0,那么不用说了,当前线程获取的是写锁。这个原因是:锁降级
三、锁降级
锁降级的最终目的是:保证共享变量的数据安全。
假如有一个线程 A 已经获取了写锁,并且修改了数据,如果当前线程 A 不获取读锁而直接释放写锁,此时,另一个线程 B 获取到了写锁并修改了数据,那么当前线程 A 无法感知线程 B 的数据更新。如果当前线程 A 获取读锁,即遵循降级的步骤(获取写锁,再获取读锁,再释放写锁),则线程 B 将会被阻塞,直到当前线程 A 使用数据并释放读锁之后,线程 B 才能获取写锁进行数据更新。
所以说,线程在持有写锁期间,在线程任务方法内,获取了读锁,再释放掉写锁,当前线程将只持有读锁。其它希望获取写锁的线程阻塞,因为读写锁ReentrantReadWriteLock是读写互斥的。其它希望获取读锁的线程不会被阻塞,因为多个线程可以共享读锁。因此,在代码中,线程在获取写锁后的任务代码中,需要获取一次读锁。
总结:如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
纸上得来终觉浅,绝知此事要躬行
/**
* Created by jay.zhou on 2018/9/14.
*/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Created by Jay.Zhou on 2018/9/14.
*/
public class ReentrantReadWriteLockDemo {
//读写锁对象,构造函数弄成false,保证是非公平锁
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);
//多个线程准备操作的共享变量
private int value;
/**
* 操作共享变量value的方法A
*/
public void writeA() {
//第一步:获取到写锁,准备写入
Lock writeLock = readWriteLock.writeLock();
//写锁加锁
writeLock.lock();
//执行任务,操作共享变量
for (int i = 0; i < 10000; i++) {
value++;
}
System.out.println("writeA操作完毕,共享变量的值是:"+value);
//任务执行完毕,获取读锁
Lock readLock = readWriteLock.readLock();
//读锁加锁
readLock.lock();
//释放写锁,当前线程就只持有读锁,可以阻塞其它的写线程
writeLock.unlock();
//让这个方法延迟5秒钟,模拟
// 1.其它读线程读取共享变量
// 2.其它写线程被阻塞
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//任务执行完毕,释放其它的写锁
readLock.unlock();
}
/**
* 操作共享变量value的方法B
*/
public void writeB() {
//写任务需要获取锁,阻塞其它线程
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
this.value = 9999;
//任务执行完毕,获取共享变量的值
System.out.println("writeB操作完毕,共享变量的值是:"+value);
writeLock.unlock();
}
//获取共享变量
public void get() {
//读取共享变量的时候,不允许写线程进入,因此需要加上读锁
Lock readLock = readWriteLock.readLock();
//上锁
readLock.lock();
//读取共享变量
System.out.println("读取任务操作完毕,共享变量的值是:"+value);
//释放读锁
readLock.unlock();
}
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(()->{
//执行写任务
//先获取写锁,再获取读锁,最后释放写锁。
//这样此线程持有的锁将会降级为读锁,阻塞其它的写入线程
//在此期间,writeB写方法将会被阻塞,而get读方法可以获取到共享变量
demo.writeA();
});
Thread.sleep(100);
//开启第二个线程
pool.execute(()->{
//第二个读线程
demo.get();
});
Thread.sleep(100);
//开启第三个线程
pool.execute(()->{
//第三个写线程尝试操作共享变量
demo.writeB();
});
pool.shutdown();
}
}
上面的程序,我执行了好几次,如果把writeB()方法放到get()方法之前执行,可能结果就有点问题。
我猜测,如果 A线程占了读锁 , 那么B写线程 将会被阻塞。 再来了一个C线程,需要排队到B线程后。
参考博客:https://blog.csdn.net/yanyan19880509/article/details/52435135