文章目录
为什么标题存在 AQS?
因为在ReentrantReadWriteLock 底层也使用到了 AQS 这个同步器框架;
abstract static class Sync extends AbstractQueuedSynchronizer {
}
ReentrantReadWriteLock 读写锁
字面意思上来看,这是一个支持重入的读写锁;
在一些业务流程中,读的操作是大于写的操作的,因此设计这种读写锁,可以在一定程度上面提高程序运行的性能;
下面提供一个数据容器类,分别使用读锁以及写锁进行数据的操作;读数据使用 read() 方法;写锁保护数据的 write() 方法;
所谓的互斥与不互斥的理解:
互斥:多个线程之间不可以同时访问一个数据
不互斥:多个线程之间可以同时访问一个数据
// 读 读之间是不互斥的 两个线程之间可以同时访问;
// 读写之间是互斥的,就是两个线程不能同时访问;
// 写 写 之间也是互斥的
// 只有有写的线程,那么线程之间就要互斥
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReaderWriteLock {
public static void main(String[] args) {
DataContainer dataContainer = new DataContainer();
new Thread(()->{
dataContainer.read();
},"t1").start();
new Thread(()->{
dataContainer.read();
},"t2").start();
}
}
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("获取读锁");
r.lock();
try {
log.debug("读取...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
} finally {
log.debug("释放读锁");
r.unlock();
}
}
public void write() {
log.debug("获取写锁");
w.lock();
try {
log.debug("写数据");
} finally {
log.debug("释放写锁");
w.unlock();
}
}
}
读写锁需要注意的地方
1、同一个线程,先获取到了读锁,然后想要获取写锁,是不支持的,会进入永久的等待中;不可以升级;
2、同一个线程,先获取到了写锁,然后获取读锁是可以的;可以降级;
源代码解读,关于 读锁以及写锁之间能不能升降级,怎么升降级的实现;
读写锁的应用
读写锁可以应用到数据库缓存的实现上,保证缓存与数据库之间的一致性;
在实际的业务场景中,因为如果每次都是同样的 sql ,并且执行的结果是一样的,每次都查数据库效率是比较低的,可以考虑使用缓存机制,提升查询效率;
使用缓存的时候,需要考虑数据的一致性以及性能的优良性;
不使用不使用读写锁实现的缓存,存在一定的缺陷,所以使用具有读写锁的方式,进行数据库缓存的实现;
注:只是数据库缓存实现的 demo ,不是真正的数据库缓存技术;
不使用读写锁的时候
先清理缓存:先更新数据库:
上面的缓存策略存在的问题:
1、数据不一致
2、加锁可以保证数据的一致性但是对于性能的损耗太大了
使用读写锁解决上面的问题
,可以保证缓存与数据库数据的一致性,也可以保证性能不会损失太多;
读写锁原理
读锁写锁的加锁原理
读写锁使用的是同一个 Sync 同步器,所以等待队列、state 也是同一个;
下面的实例中: t1 添加的是写锁, t2 添加的是读锁,进行分析;
state 分为两部分,一部分给读锁用, 一部分给写锁用;
加的结点是 SHARED 结点,不是之前的EXCLUSIVE 结点
加入到队列中还是会再尝试一次,再次失败才会 park 进入阻塞;
再次新加进来两个线程,读锁使用的是 SHARED 状态,写锁使用的是EXCLUSIVE 状态,两者之间是存在区别的;
前面三个的 WaitStatus 是 -1 后面的 WaitStatus 是 0 ,需要加以区别;
-1 代表有职责唤醒它的后继节点
写锁的解锁原理
上面的是加锁的原理,下面展示的解锁的原理:
因为读锁是可以共享的,所以可以出现 t2 t3 同时拿到一把锁的情况;
两个读锁同时出来,state 里面的前面是 2 ,表示两个读了两次,读锁占用的时候,是不能添加写的;
一个读节点,会将后面的读节点都唤醒,直到遇到写的结点了;
读锁的释放过程
-1 改为 0 ,为了唤醒后面的写结点
StampedLock 优化读功能
读写并发的情况下面,会维护 state 的状态,不能使得读达到极致,所以产生了 StampedLock 进一步的优化读功能;
在刚开始的时候不加锁,发现自己的数据被其他线程影响了之后,才会进行锁的升级;判断数据是不是被影响了是使用 stamp 这个戳来判断的; stamp 变了之后,就会进行锁的升级;
先乐观的不加锁进行读取,之后发现戳变了之后,锁升级,变为读锁;
Semaphere
信号量,用来限制能够同时访问资源的线程上限;
注意:控制的是线程数量的上限;
@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
// 创建线程执行
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("running...");
try {
Thread.sleep(1000);
log.debug("end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
// 达到了在一定时间中,限制线程的连接数量,可以减少峰值使用
21:37:28.338 c.TestSemaphore [Thread-2] - running...
21:37:28.338 c.TestSemaphore [Thread-0] - running...
21:37:28.338 c.TestSemaphore [Thread-1] - running...
21:37:29.339 c.TestSemaphore [Thread-1] - end...
21:37:29.339 c.TestSemaphore [Thread-0] - end...
21:37:29.339 c.TestSemaphore [Thread-3] - running...
21:37:29.339 c.TestSemaphore [Thread-4] - running...
21:37:29.344 c.TestSemaphore [Thread-2] - end...
21:37:29.344 c.TestSemaphore [Thread-5] - running...
21:37:30.340 c.TestSemaphore [Thread-4] - end...
21:37:30.340 c.TestSemaphore [Thread-3] - end...
21:37:30.340 c.TestSemaphore [Thread-6] - running...
21:37:30.340 c.TestSemaphore [Thread-7] - running...
21:37:30.348 c.TestSemaphore [Thread-5] - end...
21:37:30.348 c.TestSemaphore [Thread-8] - running...
21:37:31.342 c.TestSemaphore [Thread-7] - end...
21:37:31.342 c.TestSemaphore [Thread-9] - running...
21:37:31.344 c.TestSemaphore [Thread-6] - end...
21:37:31.352 c.TestSemaphore [Thread-8] - end...
21:37:32.347 c.TestSemaphore [Thread-9] - end...
Semaphore 应用
限制的是线程数量,不是限制的资源数量;
数据库连接时可以使用 Semaphore 进行优化;
Semaphore 原理
CountDownLatch
用来协作线程同步,主线程等待所有线程完成之后,主线程才会继续执行;
使用方式:
使用构造参数初始化等待数值,
await() 用来等待计数归零;
countDown() 用来让计数减去 1 的操作;
join 与 CountLatch
join 是一种比较底层的 方式,使用起来比较繁琐,推荐使用 CountLatch
CountLatch 是一种高级的 API
一把使用的线程池,
CountLatch 的应用
在多人在线游戏中,需要等待所有玩家都加入到游戏中,游戏才可以开始,可以使用线程池与 CountLatch 结合使用,主线程等待所有线程都 countDown() 等到计数数值为 0 的时候,主线程不用等待了,开始执行;
CountLatch功能:主线程可以等待线程池中的线程完成操作之后,主线程继续执行;
线程之间没有返回结果的时候,使用 CountLatch 比较方便,但是存在线程之间有返回结果的时候,需要使用 Future 进行线程结果的获取;
CyclicBarrier
对于 CountDownLatch 的一种优化,不用多次的重复创建出来 CountDownLatch对象;
注意:线程池中的线程数量和循环栅栏 CyclicBarrier 里面的设置的初始值是需要一致的,否则可能导致执行结果的不正确性;