发布时间:2022-11-02 文章分类:编程知识 投稿人:赵颖 字号: 默认 | | 超大 打印

您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~

除了四种常见的同步器(发令枪、摇号器、栅栏和交换机),JUC还有所谓线程安全的容器、阻塞队列和一些特殊的类。其中常出现的就是线程安全的容器和阻塞队列。与其说这是两个大的分类,还不如说它就是两个用得最多的类ConcurrentHashMapArrayBlockingQueue。

我的风格是尽量少讲原理,多讲实际生活中的案例,除非它非常重要,就像AQS,这玩意绝对是个重量级的神器,差不多整个JUC都是建立在它之上的。如果说学习多线程只有一次集中全部精力的机会的话,那我绝对建议把这份宝贵的精力花在AQS上,物超所值。至于搞明白ThreadPool底层原理、synchronized关键字原理,个人认为纯属浪费时间。因为即使弄明白了,那个机制你是改不了的,AQS原理弄明白了,可以直接拿来用,自己玩出花来。

从之前的集合类继承结构图可以知道ConcurrentHashMap继承自Map,实现了ConcurrentMap接口,它是线程安全的Map,侧重于放入或者获取的速度,而不在乎顺序。

因为现在的Java基本上都升级到了JDK1.8以上,有的甚至和官方保持同步,所以基本上很多JDK1.7及以下会出现的问题,在新的版本中都修复了,而且性能越来越好。所以只需要知道一点:在并发量比较高的环境中尽量使用ConcurrentHashMap就好了。不用弄明白为什么,因为不值得(JDK1.8的ConcurrentHashMap底层是用红黑树实现的,至于什么是红黑树,又得回头补一些基础,而且记住了也没啥用)。当然如果是为了应付面试,稍稍了解一下就好了。

为了给大家直观的印象ConcurrentHashMap怎么个线程安全法,这里那我之前的项目代码做个演示(当然是删除了很多实际需求代码,仅演示ConcurrentHashMap)。

电商里为了造一些刷单数据,需要模拟真实用户,那么就由代码产生好了:

/**
 * 随机姓名产生器
 *
 * @author 湘王
 */
public class RandomInfo2 {
    private static final String firstname = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻水云苏潘" +
            "葛奚范彭郎鲁韦昌马苗凤花方俞任袁柳鲍史唐费岑薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅卞齐康伍余元卜顾孟平黄和穆萧尹姚邵湛汪祁毛" +
            "禹狄米贝明臧计成戴宋茅庞熊纪舒屈项祝董粱杜阮席季麻强贾路娄危江童颜郭梅盛林刁钟徐邱骆高夏蔡田胡凌霍万柯卢莫房缪干解应宗丁" +
            "宣邓郁单杭洪包诸左石崔吉龚程邢滑裴陆荣翁荀羊甄家封芮储靳邴松井富乌焦巴弓牧隗山谷车侯伊宁仇祖武符刘景詹束龙叶幸司韶黎乔苍" +
            "双闻莘劳逄姬冉宰桂牛寿通边燕冀尚农温庄晏瞿茹习鱼容向古戈终居衡步都耿满弘国文东殴沃曾关红游盖益桓公晋楚闫";
    private static final String lastname = "伟刚勇毅俊秀娟英华慧巧美娜静淑惠珠翠雅芝玉萍红娥玲芬芳燕彩春菊兰凤洁梅琳素云莲真环" +
            "雪荣爱妹霞香月莺媛艳瑞凡佳嘉琼勤珍贞莉桂娣叶璧璐娅琦晶妍茜秋珊莎锦黛青倩婷姣婉娴瑾颖露瑶怡婵雁蓓纨仪荷丹蓉眉君琴蕊薇菁梦" +
            "岚苑婕馨瑗琰韵融园艺咏卿聪澜纯毓悦昭冰爽琬茗羽希宁欣飘育滢馥筠柔竹霭凝晓欢霄枫芸菲寒伊亚宜可姬舒影荔枝思丽峰强军平保东文" +
            "辉力明永健世广志义兴良海山仁波宁贵福生龙元全国胜学祥才发武新利清飞彬富顺信子杰涛昌成康星光天达安岩中茂进林有坚和彪博诚先" +
            "敬震振壮会思群豪心邦承乐绍功松善厚庆磊民友裕河哲江超浩亮政谦亨奇固之轮翰朗伯宏言若鸣朋斌梁栋维启克伦翔旭鹏泽晨辰士以建家" +
            "致树炎德行时泰盛雄琛钧冠策腾楠榕风航弘";
    /**
     * 随机产生姓氏
     *
     */
    public static String getFirstName() {
        int strLen = firstname.length();
        int index = new Random().nextInt(strLen - 1);
        return firstname.substring(index, index + 1);
    }
    /**
     * 随机产生名字
     *
     */
    public static String getLastName() {
        int strLen = lastname.length();
        int index = new Random().nextInt(strLen - 1);
        return lastname.substring(index, index + 2);
    }
    public static void count(final List<String> list, final Map<String, Integer> map) {
        long start = System.currentTimeMillis();
        // 这里为了比较效率,特意调大了人数
        for (int i = 0; i < 10_000_000; i++) {
            String name = getFirstName() + getLastName();
            list.add(name);
        }
        System.out.println("产生了 " + list.size() + " 个名字");
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.stream().map(str -> str.substring(0, 1)).forEach(str -> {
                        Integer count = 1;
                        if (map.containsKey(str)) {
                            // 多线程情况下,普通的HashMap的get()方法得到的结果可能为null
                            // 但ConcurrentHashMap一定不会是null
                            count = map.get(str);
                            if (null == count) {
                                System.out.println("count == null");
                            }
                            ++count;
                        }
                        map.put(str, count);
                    });
                    System.out.println("总共 " + map.size() + " 个姓氏");
                }
            }).start();
        }
        int count = 0;
        for(Map.Entry<String, Integer> entry : map.entrySet()) {
            count = count + entry.getValue();
        }
        long end = System.currentTimeMillis();
        // 最后统计名字总数
        System.out.println("执行时间:" + (end - start) + " 毫秒");
    }
    public static void main(String[] args) throws IOException {
        List<String> list = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        count(list, map);
        list = new ArrayList<>();
        map = new ConcurrentHashMap<>();
        count(list, map);
    }
}

通过这个程序,可以非常直观地看到这两者的区别。至于原理,就不必强记了,因为你不可能记住,记住了也没鸟用。

ArrayBlockingQueue和LinkedBlockingQueue在之前的线程池ThreadPool中也提过,它们的共同点都是一边放一边拿,唯一的不同是一个是数量有限的,一个是数量无限的。

Java多线程(7):JUC(下)

之前拿上菜的案例演示了ArrayBlockingQueue,现在同样的案例再用LinkedBlockingQueue来实现:

/**
 * LinkedBlockingQueue测试
 * 
 * @author 湘王
 */
public class LinkedBlockingQueueTester {
    public static BlockingQueue<String> queue = new LinkedBlockingQueue<String>(5);
    // 一个往里放
    class Producer implements Runnable {
        @Override
        public void run() {
            try {
                queue.offer("川菜");
                System.out.println(Thread.currentThread().getName() + " 厨师做好 川菜");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    // 一个往外拿
    class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                String food = queue.poll();
                System.out.println(Thread.currentThread().getName() + " 客人消费 " + food);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // 和ArrayBlockingQueue不讲究顺序不同,LinkedBlockingQueue需要将生产的一方放在前面
        // 厨师做好菜
        for (int i = 0; i < 5; i++) {
            new Thread(new LinkedBlockingQueueTester().new Producer()).start();
        }
        // 客人等着菜
        for (int i = 0; i < 5; i++) {
            new Thread(new LinkedBlockingQueueTester().new Consumer()).start();
        }
    }
}

JUC中一些其他的类,例如ForkJoinPool,可以把任务分解成更小的任务,被分解出来任务会被继续分解成更小的任务,更形象地说法是“分形器”。RecursiveTask是一种会返回结果的任务,它可以将自己分解成若干更小的任务,并将这些任务的执行结果合并到一个结果里。这两种用的不多。

整个JUC里面最重要的还是前面3种共6Java类,掌握好AQS和这6个常见类,JUC基本就没啥问题了。

感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~