java 并发-并发工具原理与使用
AQS
AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器

自定义锁
//自定义锁(不可重入)
class Mylock implements Lock{
//同步器,锁的大部分功能由下面的抽象同步器实现提供
class MySync extends AbstractQueuedSynchronizer{
@Override//尝试获取锁,尝试一次
protected boolean tryAcquire(int arg) {
//锁状态state,默认是0,改为1表示独占
if (compareAndSetState(0,1)){
//设置当前线程为owner
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override//释放锁
protected boolean tryRelease(int arg) {
//此处不用同步,因为释放锁的线程一定是持有锁的线程
//设置锁的owner为空
//state变量是volatile,exclusiveOwnerThread则不是,setState(0)放在后面,可以保证这俩变量对其他线程可见
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override//是否持有独占锁
protected boolean isHeldExclusively() {
return getState()==1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync=new MySync();
@Override//加锁
public void lock() {
//此处会调用上面重写的tryAcquire,尝试加锁一次,如果失败了,放到队列中等待,下次再来加锁
sync.acquire(1);
}
@Override//加锁,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override//尝试加锁,加锁一次失败了,就不会再加锁
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override//尝试加锁,超时等待
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override//解锁,唤醒正在排队的线程
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
ReentrantLock原理
ReentrantLock实现了Lock接口,Sycn是ReentrantLock内部的一个抽象同步器,继承自AbstractQueuedSynchronizer,Sycn由两个实现NonFairSync和FairSync,默认非公平,可创建公平锁。

非公平锁的实现原理
第一个线程来,没有竞争。


接着第二个线程来,有竞争,尝试加锁,失败,执行上面的acquire(1),再尝试获取,若失败,会调用
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)


接下来会创建一个链表,节点之间为双向,会关联线程对象,这其实一个等待队列。图里的head和tail不是节点,head指向的是头节点(head),tail指向尾节点


第3点解释,Dummy的waitStatus改为-1,那么以后有Dummy来唤醒线程1,因为线程1一直获取锁失败后会进入队列等待,需要有能唤醒的节点。如果再来一个线程2,失败了,那么节点线程1的waitStatus改为-1,有线程1唤醒线程2,这就是前驱节点唤醒后继节点。

park,进入队列阻塞等待

多个线程竞争失败

释放锁


此时有两种情况,
1.成功获取锁


2.新来了一个线程4(注意,不在队列的),竞争锁,线程1失败,进入重新抢锁的流程。

可重入原理
同一个线程,锁重入的过程

同一个线程释放锁的过程,入参是1,关键是锁重入次数(state)减去1。举例说明,state为5,表示重入了4次,5减了4次,最后为1表示当前线程要释放锁了。

可打断原理
先从1.不可打断模式开始理解
总的来说,线程被打断后,调用Thread.interrupted()被打断后会返回true(打断标记为true),表示被打断了,但是同时又将打断标记设为true,接下来又再一次进入循环,又开始获取锁,又可以park进入等待获取锁。


2.打断模式
直接抛异常,不再获取锁

公平锁
1.先了解下非公平锁
线程上来就直接抢锁,不会检查等待队列

2.公平锁


条件变量
await

线程0调用ConditionObject的await方法,进入等待的流程

接着,线程0进入队列的节点,节点状态(黄色三角)为-2,表示条件变量的等待,然后将线程0持有的锁全部释放(因为可能发生锁重入了),同步器的state设为0(释放了)

没有其他新来的线程竞争锁的话,线程1获取锁

signal
唤醒开始,由持有锁的线程才能唤醒,否则会抛异常

取条件变量等待中的第一个,此例中为线程0,尝试唤醒线程0,如果成功进入等待队列抢锁,也有可能失败,因为线程0可能被打断,也有可能超时等待不再获取锁了,此时会再找下一个在条件变量的节点(线程)

成功的情况

读写锁
ReentrantReadWriteLock
读-写、写-读,这俩操作加锁,读-读不用加锁。默认非公平,通过构造函数传入 true,即可创建公平锁。由于读读不加锁,体现了共享锁的思想。

读-读示例
读操作获取读锁,模拟读时长1秒,可以看到2个线程几乎能同时获取锁,进行读取,1秒后都释放锁,可以看到,不互斥。


读-写示例
w.lock()要获取写锁,要等1秒后,读锁释放。



写-读示例
先写,等写完再读
@Slf4j(topic = "c.test")
public class Test41 {
public static void main(String[] args) throws InterruptedException {
ReadWriteLock lock=new ReadWriteLock();
new Thread(()->{
log.debug("启动");
lock.write();
},"t1").start();
Thread.sleep(100);
new Thread(()->{
log.debug("启动");
lock.read();
},"t2").start();
}
}
@Slf4j(topic = "c.test")
class ReadWriteLock{
private Object data;
private ReentrantReadWriteLock rw=new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read(){
r.lock();
try {
log.debug("开始读");
log.debug("读完");
}finally {
r.unlock();
}
return data;
}
public void write(){
w.lock();
try {
log.debug("开始写");
Thread.sleep(2000);
log.debug("写完");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
}
}
输出:
10:55:26.538 [t1] c.test - 启动
10:55:26.539 [t1] c.test - 开始写
10:55:26.649 [t2] c.test - 启动
10:55:28.544 [t1] c.test - 写完
10:55:28.544 [t2] c.test - 开始读
10:55:28.544 [t2] c.test - 读完
注意事项
读锁不支持条件变量,写锁支持
同一个线程不能持有读锁的情况下,再获取写锁。

支持降级,体现了锁重入。

示例代码说明降级升级:
class CachedData {
//缓存的数据
Object data;
//缓存是否有效,true有效
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
//获取读锁
rwl.readLock().lock();
//情况1.缓存失效
if (!cacheValid) {
// 在获取写锁之前必须释放读锁,因为不支持锁升级
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 重新检查状态,因为另一个线程可能在我们之前已经获取了写锁并更改了状态。举个例子,
//线程1执行完rwl.readLock().unlock(),准备执行rwl.writeLock().lock(),线程2就执行了
//rwl.writeLock().lock(),并且改了数据,缓存失效,cacheValid为false
//线程2改完,线程1也要改,他改他的,我改我的
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 通过在释放写锁之前获取读锁来进行锁降级,这样能让其他线程读取缓存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放写锁,仍然持有读锁
}
}
//情况2.缓存有效,使用或处理数据
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
读写锁应用之缓存
读写锁的原理
stampedlock
介绍

演示
write,写方法,加锁写,返回时间戳,写完后,解锁,再将时间戳传入,时间戳已改

read,读方法,readTime只是为了测试用,tryOptimisticRead读取锁的时间戳;
举例说明:
情况1: 线程1和线程2同时读取时间戳,接下来验证戳有没有改变,没有改变,直接返回数据。
情况2: 线程1和线程2同时读取时间戳,线程3写操作,改了时间戳,线程1、2接下来验证戳有没有改变,改变,readLock加上读锁,返回最新的数据。

Semaphore

代码演示
可创建是否公平

如果不使用信号量,10个线程同时执行,10个线程同时启动运行,1秒后都结束。


使用信号量,限制为至多3个线程同时运行,acquire方法要放在线程的函数方法内,才能起作用。

3个线程运行后,其他的才能运行

原理
假设最多允许3个线程,state为3,state就是之前写的AQS同步器里的变量state

acquire原理

release原理

Semaphore应用之改进连接池
CountDownLatch
介绍,倒计锁

示例代码,下面的代码含义,倒数计数值为3,主线程调用latch.await方法,主线程等待,会卡在这一行,等三个线程执行完。一个线程执行后将倒数计数减1,三个线程执行完后,倒数计数为0,主线程结束等待。
@Slf4j(topic = "c.test")
public class Test43 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch =new CountDownLatch(3);
log.debug("wait");
new Thread(() -> {
log.debug("running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
},"t1").start();
new Thread(() -> {
log.debug("running");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
},"t2").start();
new Thread(() -> {
log.debug("running");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
},"t3").start();
latch.await();
log.debug("end wait");
}
}
如果加多一个线程t4(执行了6秒),倒计数还是3,那么t1、t2、t3个线程执行完,就结束等待,不会等t4。
new Thread(() -> {
log.debug("running");
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
},"t4").start();
如果有2个线程,倒计数还是3,那么主线程就不会结束,因为计数最后为1,不为0
下面是错误用法,代码一开始就减去1,会导致线程没执行完,主线程就结束等待。必须线程执行完才减去1

在线程池中使用CountDownLatch
4个线程,线程1、2、3运行,线程4等待前面3个线程运行结束
@Slf4j(topic = "c.test")
public class Test44 {
public static void main(String[] args) {
CountDownLatch latch =new CountDownLatch(3);
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
log.debug("running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
});
pool.submit(() -> {
log.debug("running");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
});
pool.submit(() -> {
log.debug("running");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
latch.countDown();
log.debug("end");
});
//第4个线程等待前面的3个线程结束
pool.submit(() -> {
try {
log.debug("wait");
latch.await();
log.debug("end wait");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
pool.shutdown();
}
}
输出如下:
17:37:32.109 [pool-1-thread-1] c.test - running
17:37:32.109 [pool-1-thread-3] c.test - running
17:37:32.109 [pool-1-thread-2] c.test - running
17:37:32.109 [pool-1-thread-4] c.test - wait
17:37:33.112 [pool-1-thread-1] c.test - end
17:37:34.126 [pool-1-thread-3] c.test - end
17:37:35.114 [pool-1-thread-2] c.test - end
17:37:35.114 [pool-1-thread-4] c.test - end wait
浙公网安备 33010602011771号