管程
管程是管理共享变量和对共享变量操作的过程;上文讲解了synchronized在jvm层面实现了MESA管程模型,本文阐述AQS在java层面实现MESA管程模型
AQS--AbstractQueuedSynchronizer
AQS是java层面实现管程的同步队列抽象类,抽象类实现管程的同步等待队列和条件等待队列;而加锁和解锁的逻辑由具体的子类实现,只是对外提供模板方法,由程序员自己实现加解锁的逻辑。
AQS特性
阻塞等待队列;共享/独占;公平/非公平;可重入;可中断
AQS内部核心
state变量:是volatile修饰的int类型,能否获取锁就是通过CAS操作设置state变量来控制
同步等待队列:CAS尝试修改state相当于竞争锁,获得锁成功则执行业务逻辑,竞争锁失败则进入同步等待队列并调用LockSupport.park()方法阻塞当前线程,等待其他获得锁的线程调用LockSupport.unpark()方法唤醒该线程并出队;同步等待队列是双向链表结构
条件等待队列:Condition,await()/signal()、signalAll(),阻塞唤醒机制来对线程进行入队出队操作,单向链表结构
Condition接口:await()/signal()、signalAll()
两种共享资源:
SHARED-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
EXCLUSIVE-独占,只有一个线程能执行,如ReentrantLock
中断:线程t1调用lockInterruptibly()方法竞争锁,此时锁已经被t2占有,t1竞争锁失败被阻塞,t2执行完了并调用t1.interrupt()中断线程t1,t1获取锁失败会抛出中断异常,最终执行catch逻辑。
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
try {
lock.lockInterruptibly();
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("t1等锁的过程中被中断");
}
}, "t1");
lock.lock();
try {
log.debug("main线程获得了锁");
t1.start();
Thread.sleep(1000);
t1.interrupt();
log.debug("线程t1执行中断");
} finally {
lock.unlock();
}
}
ReentrantLock
ReentrantLock是基于AQS的互斥锁实现的一种线程并发访问的同步手段,它的功能类似与synchronized是一种互斥锁,可以保证线程安全。
ReentrantLock与synchronized优缺点和异同点
1、ReentrantLock实现了公平锁和非公平锁,而synchronized是非公平锁(先进后出的栈结构通过单向链表实现)
2、ReentrantLock可以显示的控制加锁和解锁的逻辑,synchronized基于jvm实现隐示自动加解锁
3、synchronized是不可以被中断的,ReentrantLock是可以被中断的(lockInterruptibly())
4、ReentrantLock获得锁的方式灵活多变如:lock(),tryLock(),tryLock(time, unit),lockInterruptibly(),synchronized能修饰方法和代码块。
5、它们都是可重入锁
6、synchronized是基于jvm实现,ReentrantLock是jdk实现的
可重入:thread线程变量记录判断是否同一线程进入,表示可重入
可中断
锁超时:tryLock()--立即超时,tryLock(time, unit)--超时时间
条件变量
ReentrantLock源码执行逻辑跟踪
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(()->{
lock.lock();
try {
for (int j = 0; j < 10000; j++) {
sum++;
}
} finally {
lock.unlock();
}
});
thread.start();
}
Thread.sleep(2000);
System.out.println(sum);
竞争锁的逻辑流程
当t1和t2都入队以后,t0释放锁,将state设置为0,并将AQS的exclusiveOwnerThread属性设置为null,设置waitStatus为0,并调用LockSupport.unpark(t1)唤醒t1线程
t1唤醒后将thread属性设置为null,然后调用tryLock()方法竞争锁,此时state=0,将AQS的head属性设置为node1,并将node0和node1之间的next和prev属性置为null,相当于将node0剔除,至此t1获得锁。
t2获得锁的流程与t1一样。
Semaphore
Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它是基于AbstractQueuedSynchronizer(共享锁和非公平锁模式)实现的。
Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
应用场景
可以用于做流量控制,特别是公用资源有限的应用场景
public class SemaphoreTest {
private static ThreadPoolExecutor pool = new
ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200));
private static Semaphore semaphore = new Semaphore(3);
static class exec implements Runnable {
@Override
public void run() {
try {
semaphore.acquire();
log.debug("{} execute exec...", Thread.currentThread().getName());
Thread.sleep(2000);
log.debug("{} execute finish", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
log.debug("{} release lock", Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
pool.execute(new exec());
}
}
}
new Semaphore(3)表示型号量资源设置为3,也就是说AQS中属性state=3,意思是每一时刻只会有3个线程会执行任务。循环创建5个任务丢给线程池执行,但是只会有3个线程获得共享锁执行任务。假设这里会t0,t1,t2都获得共享锁执行任务,而没有释放锁。5个线程竞争锁成功和失败入队阻塞的流程:
锁竞争结束后,此时假设t0任务执行完成并释放锁成功(也就是AQS中state+1操作),那么会执行AQS#doReleaseShared():拿到头结点,判断头结点waitStatus是否为-1,是则将waitStatus通过CAS设置0并唤醒t3,此时t3就尝试获取锁并且获取锁成功,将node0节点移除队列并会唤醒线程t4然后执行任务,此时如果t1释放锁成功了,那么t4就会获得资源,如果这里没有获得锁,t4会继续入队挂起。
CountDownLatch
CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。CountDownLatch是通过AQS的“共享锁”实现
CountDownLatch应用场景
CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。
CountDownLatch的两种使用场景:
场景1:让多个线程等待;模拟并发,让并发线程一起执行
public class CountDownLatchTest1 {
private static CountDownLatch count = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{} ready...", Thread.currentThread().getName());
}).start();
}
log.debug("{}等待所有线程准备好.", Thread.currentThread().getName());
Thread.sleep(2000);
count.countDown();
}
}
场景2:让单个线程等待;多个线程(任务)完成后,进行汇总合并
public class CountDownLatchTest {
private static CountDownLatch count = new CountDownLatch(5);
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i < 6; i++) {
int finalI = i;
new Thread(() -> {
try {
Thread.sleep(finalI * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{} ready...", Thread.currentThread().getName());
count.countDown();
}).start();
}
count.await();
log.debug("{} success.", Thread.currentThread().getName());
}
}
CountDownLatch与Semaphore一样都是通过AQS的共享锁实现的;取场景二举例,main线程会初始化执行new CountDownLatch(5)(这里设置AQS的state属性等于5),main线程在执行count.await()时,判断state值是否等于0,不是则直接返回-1去创建两个节点并首位相连组成双向链表(和Semaphore获取锁失败一样),最后挂起main线程;其他5个线程会依次执行count.countDown(),5个线程每次调用该方法都会调用AQS#tryReleaseShared(1)方法将state值减1,直到最后一个线程将state设置为0并执行doReleaseShared()方法去唤醒main线程。
场景一中,5个线程调用count.await()会创建一个双向链表并将所有线程挂起,main线程调用Thread.sleep(2000)后调用count.countDown()会唤醒(底层LockSupport.unpark(thread))链表中头结点的下一个节点的线程,下个节点的线程会依次唤醒下一个节点线程直到最后一个节点线程。
CyclicBarrier
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的。
应用场景
计算每个人的平均分,最后统计所有人的平均分
public class CyclicBarrierTest1 {
private Map<String, Integer> map = new ConcurrentHashMap<>();
private Executor executor = Executors.newFixedThreadPool(3);
private CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
int result = 0;
Set<String> set = map.keySet();
for (String s : set) {
result += map.get(s);
}
log.debug("三人平均成绩为{}分", result/3);
});
public void count() {
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
int score = (int)(Math.random()*40+60);
map.put(Thread.currentThread().getName(), score);
log.debug("{}平均成绩为:{}", Thread.currentThread().getName(), score);
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
public static void main(String[] args) {
CyclicBarrierTest1 cb = new CyclicBarrierTest1();
cb.count();
}
}
场景跟踪分析流程图
关注点:
1、t0和t1创建条件队列和入队,以及释放锁阻塞
2、t2如何将条件队列转为同步等待队列并唤醒t0线程