黑马并发编程JUC(信号量、线程安全类)总结

9.JUC

Semaphore

为什么需要用到Semaphore?

限流

Sdmaphore的场景?

秒杀商品的时候,不能够让那些没有秒杀成功的线程进入,只有占了坑位的才可以使用,这里可以用redis来记录这个Semaphre

Semaphore的原理?

AQS+state进行分析

定义

信号量,相当于就是停车位限制流量。

  • acquire:拿到位置
  • release:释放位置
@Slf4j(topic = "c.test")
public class MyTestSemaphore {
   
    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");
                Sleeper.sleep(1);

                log.debug("end");
                semaphore.release();
            }).start();



        }
    }
}

原理

其实都是AQS的原理

acquire
  • acquire调用了sync(nonfairSync)的acquireSharedInterruptibly(1);尝试上锁tryAcquireShared(arg)
  • 调用nonfairTryAcquireShared(int acquires),每次都是-1,直到信号量小于1的时候(相当于就是AQS的state==0的时候)直接返回这个remaining。
  • 如果得到返回值是<0,那么就把对应线程送进阻塞队列。基本和lock操作一样。
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
   
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
static final class NonfairSync extends Sync {
   
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
   
            super(permits);
        }

    //尝试获取锁
        protected int tryAcquireShared(int acquires) {
   
            return nonfairTryAcquireShared(acquires);
        }
    }

//非公平获取锁
final int nonfairTryAcquireShared(int acquires) {
   
            for (;;) {
   
                int available = getState();
                int remaining = available - acquires;//直接就是-1
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

image-20211016235851216

release
  • release->调用releaseShare(包括了tryReleaseShared和doReleaseShared)
  • 处理逻辑相似,但是tryReleaseShared是把state+1而不是-1,原因就是信号量的state的意思是有多少坑位可以使用,也就是只要有坑位,那么线程就能获取锁。(代码非常简洁)
 protected final boolean tryReleaseShared(int releases) {
   
            for (;;) {
   
                int current = getState();
                int next = current + releases;//坑位+1《释放了一个
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

image-20211017101036462

总结:acquire调用acquireSharedInterruptibly(可被中断)。然后就是模板tryAcquireShare和doAcquireSharedInterruptibly(和doAcquire差不多的逻辑,但是多了一个唤醒共享setHeadAndProgation)锁的方法(读锁)。

CountDownLatch

为什么需要用到CountDownLatch?

应用场景:一个线程需要等待多个线程结果的时候。或者需要等待其它线程运行完之后

定义

他就是一个倒计时锁,await之后需要等待countDown到0的时候才会解锁。

@Slf4j(topic = "c.test")
public class TestCountDownLock {
   
    public static void main(String[] args) {
   
        CountDownLatch countDownLatch = new CountDownLatch(3);

        new Thread(()->{
   
            log.debug("begin1");
            Sleeper.sleep(1);
            log.debug("end2");
            countDownLatch.countDown();
        },"t1").start();

        new Thread(()->{
   
            log.debug("begin2");
            Sleeper.sleep(1);
            log.debug("end2");

            countDownLatch.countDown();
        },"t2").start();

        new Thread(()->{
   

            log.debug("begin3");
            Sleeper.sleep(2);
            log.debug("end3");
            countDownLatch.countDown();

        },"t3").start();

        try {
   
            countDownLatch.await();
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }

        log.debug("结束");
    }


}

join同样可以完成功能,但是万一线程阻塞了,那么就会导致最后的join一直处于等待,需要进行特殊的处理。但是CountDownLatch能够进行倒计时,只要倒计时结束,那么就会结束主线程的阻塞

public static void main(String[] args) {
   
        ExecutorService service = Executors.newFixedThreadPool(4);
        CountDownLatch countDownLatch=new CountDownLatch(3);
        service.submit(()->{
   
            log.debug("begin1");
            Sleeper.sleep(1);
            log.debug("end1");
            countDownLatch.countDown();
        },"t1");



        service.submit(()->{
   
            log.debug("begin2");
            Sleeper.sleep(1);
            log.debug("end2");
            countDownLatch.countDown();
        },"t2");
        service.submit(()->{
   
            log.debug("begin3");
            Sleeper.sleep(1);
            log.debug("end3");
            countDownLatch.countDown();
        },"t3");
        service.submit(()->{
   
            log.debug("await");
            try {
   
                countDownLatch.await();
            } catch (InterruptedException e) {
   
                e.printStackTrace();
            }
            log.debug("解锁");
        },"t4");
    }

游戏案例

为什么加载的时候需要使用到countDownLock?

原因就是多方面,多文件需要加载,需要全部文件和配置初始化之后才能够进行开始,所以可以使用CountDown来记录最终需要等待的文件以及线程数。在谷粒商城的获取商品信息、快递信息的时候都会用到这种方式来提高访问的速度,并发执行,并且通过countDown来记录要执行完任务的个数才能够继续往下面执行。也可以使用join或者是future的getAll来进行阻塞。

public static void test6(){
   
        String[] a=new String[10];
        Random random = new Random();
        ExecutorService service = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch=new CountDownLatch(10);

        for(int j=0;j<10;j++){
   
            int k=j;
            service.submit(()->{
   
                for(int i=0;i<=100;i++){
   
                    try {
   
                        Thread.sleep(random.nextInt(100));
                    } catch (InterruptedException e) {
   
                        e.printStackTrace();
                    }
                    a[k]=i+"%";
                    System.out.print("\r"+Arrays.toString(a));
                }
                countDownLatch.countDown();
            },"t"+j);
        }

        try {
   
            countDownLatch.await();
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        log.debug("游戏开始");

    }

商品问题如果并发执行完且获取结果再继续执行?

还是需要future的get处理。没有结果通常使用countdownlatch

那么CountDownLatch有什么问题?

问题就是它不能够重置countdown的数量,也就是多次循环的话每次都要new一个,而不能够重用对象。

解决办法就是CyclicBarrier,能够重用,而且可以执行最终的方法。

线程数有什么要求?

必须和循环任务数相同,不然就会多个任务被线程开启。假设3线程,3次循环,两个任务,那么就会取出第一次,第二次任务执行,还会取出循环的下一次任务执行。因为线程多。

@Slf4j(topic = "c.test")
public class TestCycleBarrier1 {
   
    public static void main(String[] args) {
   
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2,()->{
   
            log.debug("结束");
        });
        ExecutorService service = Executors.newFixedThreadPool(2);
        for(int i=0;i<3;i++){
   
            service.submit(()->{
   
                log.debug("开始....");
                Sleeper.sleep(1);
                try {
   
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
   
                    e.printStackTrace();
                }
            },"t1")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值