java实现管程之AQS以及ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier

管程

管程是管理共享变量和对共享变量操作的过程;上文讲解了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线程

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值