• Java并发编程(一)JUC同步类


    JUC 是学习 Java 并发编程的小伙伴不可避免的一个 pkg,JUC提供了对并发编程的底层支持,比如我们熟悉的线程池、MQ、线程同步... 都有JUC的影子,下面我们一起来看看JUC下比较重要的几个class。

    CountdownLatch

    先看一下 **latch **是什么意思:
    image.png
    门闩大家都知道吧,假设我们现在有一扇铁门,每个线程跑到这铁门时都取下一把门闩,当所有的门闩都从门上取下来时,门就打开了,线程就可以继续往下执行。

    public class CountdownLatchTest {
        public static void main(String[] args) throws InterruptedException {
            // 传入放多少个门闩
            CountDownLatch cdl = new CountDownLatch(10);
            
            // 启 10 个线程
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + " 取下一把门闩");
                    // 每个线程运行完就取下一把门闩 
                    cdl.countDown();
                }, "线程" + i).start();
            }
            // 铁门在这等着门闩被取完了才打开
            cdl.await();
    
            System.out.println("门打开了");
        }
    }
    

    运行结果:
    image.png
    在实际开发中,我们常用多个门闩来对多个线程的执行顺序进行控制,后面有机会给大家分享一下案例。

    CyclicBarrier

    我们要搞清楚一个东西肯定得知道他是什么意思,大佬对命名都是很讲究的。(我大学刚接触编程都是 a,b,c,d,e,f,g命名)
    image.pngimage.png
    单词每个都认识,连在一起就不知道是什么意思了。
    循环的障碍?循环的屏障?其实都无所谓,自己挑一个容易理解的名字即可,我理解的是“循环的栅栏”
    什么是栅栏?
    就是拦在你前面的一个障碍物,这个障碍物你一个人推不开,得多叫几个兄弟一起来推才行,推开了才能继续往前走。

    public class CyclicBarrierTest {
        public static void main(String[] args) {
            CyclicBarrier cb = new CyclicBarrier(10, () -> {
                // todo:人齐了该干嘛
                System.out.println("人齐了,开冲");
            });
            
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "已就位");
                    try {
                        // 线程都在这里等着,直到人气才会执行 CyclicBarrier 里面的方法
                        cb.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }, i + " 号兄弟").start();
            }
        }
    }
    

    运行结果:
    image.png
    CountDownLatch 不同的是,CyclicBarrier 还有个 Cyclic ,我相信你已经猜到了,我们启动 20 个线程看看
    image.png
    门闩是一次性的,当门闩全部取下来这个门就打开了,而栅栏是有弹性的,当人满了把它推开后,它又弹回来把路又堵住了,又需要 n 个人才能把栅栏推开。
    如果有小伙伴感兴趣可以去看下它是怎么实现循环的

    Semaphore

    Semaphore 的意思是:信号量,信号量在线程同步中也是很常见的,你可以把它看作是一个 计数器 cnt

    • 当 cnt > 0 说明还有资源
    • 当 cnt < 0 说明没有资源了

    举个栗子:比如办公室有一台打印机,但是今天领导让大家写一份加薪报告,大家都忙着用这台打印机打印自己的报告,准备今晚加餐吃鸡腿。

    public class SemaphoreTest {
        public static void main(String[] args) {
            // 只有一台打印机
            Semaphore s = new Semaphore(1);
    
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    try {
                        // 去抢打印机(不一定抢得到)
                        s.acquire();
                        System.out.println(Thread.currentThread().getName() + " 拿到打印机了,今晚可以加鸡腿了");
                        // 把打印机还回去
                        s.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }, "员工 " + i).start();
            }
        }
    
    }
    

    输出结果:
    image.png
    **Semaphore **适用于资源有限,消费者无限的场景,比如打印机就是一个很好的例子。
    因为我初始化的资源是 1 ,所以会让你觉得和 差不多,其实不然
    资源还可以初始化为 10 20 50 100.... 你可能想到了什么,没错,就是限流!
    有大量的请求进来,我们只允许我们指定的数量请求进行处理,更多的用途就等以后你来开发了

    ReadWriteLock

    读写锁
    读锁:也就是共享锁,这把锁允许多个线程持有
    写锁:也就是独占锁,这把锁只允许一个线程拥有
    说人话的意思就是:读不加锁,写加锁
    我们都知道加锁的效率会比较低,如果我们只是对资源进行读取,其实是没有线程安全的问题,这时候再加锁就显得有点脱了裤子放屁(多此一举)
    所以我们就有了读写锁:读不加锁,写加锁

    public class ReadWriteLockTest {
        private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // 读写锁通过 ReentrantReadWriteLock 实例进行创建,说明读写锁也是可重入的
        private static final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        private static final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    
        public static void main(String[] args) {
            ReadWriteLockTest o = new ReadWriteLockTest();
    
            // 先把 写方法注释掉,模拟读操作
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    read();
                    //write();
                }, "线程" + i).start();
            }
    
    
    
    
        }
    
        public static void read() {
            // 上读锁
            readLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 开始读取数据");
                System.out.println(Thread.currentThread().getName() + " 读取数据成功");
            } finally {
                readLock.unlock();
            }
    
        }
    
    
        public static void write() {
            writeLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "开始写入数据");
                System.out.println(Thread.currentThread().getName() + "写入数据成功");
            } finally {
                writeLock.unlock();
            }
        }
    }
    

    读操作结果:
    image.png
    我们可以看到执行顺序,线程 8 还没读取成功,线程 9 又在读取数据了。
    写操作结果:
    image.png
    我们可以看到,虽然线程不是顺序执行的,但是 新的线程 一定是 等上一个线程写入成功后,自己才开始写如数据
    也就是说,开始写入和写入成功永远是成对出现。

    BlockingQueue

    阻塞队列:相信大家对 Java 中的容器一定不陌生,容器又分了两类:Collection && Map
    Collection 中的 List && Set ** **大家都已经耳熟能详了,但是 Queue 可能很少用到
    但是我可以告诉你,Queue 的重要程度丝毫不亚于你所知道另外几种
    BlockingQueue 更是 线程池MQ 发展的基石
    复习一下 Queue,Queue 是一个队列:从尾进,从头出(先进先出)
    Queue 给我们提供了几个API:

    • add() 添加一个元素,队列满则报错
    • offer() 添加一个元素,队列满不报错
    • poll() 移除队头元素
    • peek() 获取队头元素

    而BlockingQueue 给我们提供了几个阻塞的API:

    • put() 往队列里面放元素,如果队列满,就一直等着(阻塞),直到队列空出来位置。
    • take() 往队列里面取元素,如果队列为空,就一直等着(阻塞),直到队列有元素加进来

    还记得线程池核心参数中的任务队列吗?就是让我们传入一个 BlockingQueue
    BlockingQueue 分为有界和无界两种:

    • 有界: ArrayBlockingQueue (数组实现,要指定数组长度,当然有界啦)
    • 无界:LinkedBlockingQueue(链表实现,新任务直接挂在尾节点上,当然就无界了)

    说了这么多,我们浅写一个 生产者/消费者 模型:

    public class BlockingQueueTest {
        public static void main(String[] args) {
            // 实例化一个容量为 5 的 BlockingQueue
            ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<>(5);
    
            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    try {
                        abq.put(i);
                        System.out.println("生产者生产数据:" + i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
    
            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    try {
                        Integer take = abq.take();
                        System.out.println("消费者消费数据:" + take);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
        }
    }
    

    运行结果:
    image.png
    这就是一个简单的 MQ

    SynchronousQueue

    SynchronousQueue 是一个很特殊的 BlockingQueue,特殊在:
    image.png
    它是没有容量,也就是说:生产者把生产出来的数据,手把手递给消费者才算完成

    conclusion

    其实还有 DelayQueue(延时队列),TransferQueue(控制生产者生产速率用) ... 没有讲,今天我累了 TAT,下节准备分享一下 线程池相关的知识,铁铁们,点个赞再走吧。

  • 相关阅读:
    Linux内核将要支持最新龙芯3A2000/3B2000
    微软拥抱Linux,着实太晚了
    武校学生
    第一篇 SCI 综述被接收的感想
    如何使用Rally+Docker测试OpenStack
    (OK) ntp——linux设置系统时间—RHEL—FEDORA—CENTOS
    理解 Linux 网络栈(2):非虚拟化Linux 环境中的 Segmentation Offloading 技术
    (OK) find-alter-files.sh——递归
    (OK) digui-gb18030-utf8.sh——递归
    (OK) digui-dir-del-M.sh——递归
  • 原文地址:https://www.cnblogs.com/Fzeng/p/16175041.html
Copyright © 2020-2023  润新知