• 第101次提醒:++ 不是线程安全的


    疯狂创客圈 Java 分布式聊天室【 亿级流量】实战系列之 -17【 博客园 总入口



    源码IDEA工程获取链接Java 聊天室 实战 源码

    写在前面

    ​ 大家好,我是作者尼恩。

    目前正在组织 疯狂创客圈的几个兄弟,从0开始进行高并发的100级流量(不是用户)聊天器的实战。
    

    在设计客户端之前,发现一个非常重要的基础知识点,没有讲到。这个知识点就是Java并发包。

    由于Java并发包将被频繁使用到,所以不得不停下来,先介绍一下。

    一道简单线程安全题,不知道有多少人答不上来

    尼恩作为技术主管,常常组织组织技术面试,而且往往是第二面。

    某次面试,候选人是从重庆一所211大学毕业了一年的初级Java工程师,暂且简称Y君。

    在尼恩面试前,Y君已经过了第一关,通过了PM同事的技术面试,PM同事甚至还反馈说Y君的继承不错。理论上,Y君的offer已经没有什么悬念了。

    于是,尼恩想前面无数次面试一样,首先开始了多线程方面的问题。

    先上来就是砸出一个古老的面试问题:

    程序为什么要用多线程,单线程不是很好吗?

    多线程有什么意义?

    多线程会带来哪些问题,如何解决?

    ++操作是线程安全的吗?

    乖乖,Y君的答案,令人出人意料。

    答曰:“我从来没有用过多线,不是太清楚多线程的意义,也不清楚多线程能带来哪些问题”。

    乖乖,看一看Y君的简历,这个又是一个埋头干活,被增删改查坑害了的小兄弟!

    这已经不是第一个了,我已经记不清楚,有多少面试的兄弟,搞不清楚一这些非常基础的并发编程的知识。

    单体WEB应用的时代,已经离我们远去了。 微服务、异步架构的分布式应用时代,已经全面开启。

    对于那些面试失败的兄弟,为了提升他们的水平,尼恩都会给他提一个善意的建议。让他们去做一个简单的并发自增运算的实验,看看自增运算是否线程安全的。

    实验:并发的自增运算

    使用10条线程,对一个共享的变量,每条线程自增100万次。看看最终的结果,是不是1000万?

    完成这个小实验,就知道++运算是否是线程安全的了。

    实验代码如下:

    /**
     * Created by 尼恩 at 疯狂创客圈
     */
    
    package com.crazymakercircle.operator;
    
    import com.crazymakercircle.util.Print;
    
    /**
     * 不安全的自增 运算
     */
    public class NotSafePlus
    {
        public static final int MAX_TURN = 1000000;
    
        static class NotSafeCounter implements Runnable {
            public  int amount = 0;
    
            public void increase() {
                amount++;
            }
    
            @Override
            public void run() {
                int turn = 0;
                while (turn < MAX_TURN) {
                    ++turn;
                    increase();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            NotSafeCounter counter=new NotSafeCounter();
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(counter);
                thread.start();
            }
            Thread.sleep(2000);
            Print.tcfo("理论结果:" + MAX_TURN * 10);
            Print.tcfo("实际结果:" + counter.amount);
            Print.tcfo("差距是:" + (MAX_TURN * 10 - counter.amount));
        }
    }
    

    运行程序,输出的结果是:

    [main|NotSafePlus:main]:理论结果:10000000
    
    [main|NotSafePlus:main]:实际结果:9264046
    
    [main|NotSafePlus:main]:差距是:735954
    

    也就是说,并发执行后,总计自增1000万次,结果少了70多万次,差距是巨大的,在10%左右。

    当然,这只是一次结果,每一次运行,差距都是不同的。大家可以动手运行体验一下。

    从结果可以看出,自增运算符不是线程安全的。

    ++ 运算的原理

    自增运算符,至少包括三个JVM指令

    • 从内存取值

    • 寄存器增加1

    • 存值到内存

      这三个指令,在JVM内部,是独立进行的,中间完全可能会出现多个线程并发进行。

    比如:当amount=100是,有三个线程读同一时间取值,读到的都是100,增加1后结果为101,三个线程都存值到amount的内存,amount的结果是101,而不是103。

    JVM内部,从内存取值,寄存器增加1,存值到内存,这三个操作自身是不可以再分的,这三个操作具备原子性,是线程安全的,也叫原子操作。两个、或者两个以上的原子操作合在一起进行,就不在具备原子性。比如先读后写,那么就有可能在读之后,这个变量被修改过,写入后就出现了数据不一致的情况。

    Java 的原子操作类

    对于每一种基本类型,在java 的并发包中,提供了一组线程安全的原子操作类。

    对于Integer类型,对应的原子操作类是AtomicInteger 类。

    java.util.concurrent.atomic.AtomicInteger
    

    使用 AtomicInteger类,实现上面的实验,代码如下:

    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 安全的 ++ 运算
     */
    public class SafePlus
    {
        public static final int MAX_TURN = 1000000;
    
        static class NotSafeCounter implements Runnable {
            public AtomicInteger amount =
                    new AtomicInteger(0);
    
            public void increase() {
                amount.incrementAndGet();
            }
    
            @Override
            public void run() {
                int turn = 0;
                while (turn < MAX_TURN) {
                    ++turn;
                    increase();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            NotSafeCounter counter=new NotSafeCounter();
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(counter);
                thread.start();
            }
            Thread.sleep(2000);
            Print.tcfo("理论结果:" + MAX_TURN * 10);
            Print.tcfo("实际结果:" + counter.amount);
            Print.tcfo("差距是:" + (MAX_TURN * 10 - counter.amount.get()));
        }
    }
    

    运行代码,结果如下;

    [main|NotSafePlus:main]:理论结果:10000000
    
    [main|NotSafePlus:main]:实际结果:10000000
    
    [main|NotSafePlus:main]:差距是:0
    
    

    这一次,10条线程,累加1000w次,结果是1000w。

    看起来,如果需要线程安全,需要使用Java并发包中的原子类。

    写在最后

    ​ 下一篇:Netty 中的Future 回调实现与线程池详解。这个也是一个非常重要的基础篇。


    疯狂创客圈 Java 死磕系列

    • Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战

    
    
  • 相关阅读:
    Task10 文本预处理
    Task09 批量归一化
    Task06 Basic of CNN
    Task05 梯度消失和梯度爆炸
    Task 04 过拟合,欠拟合及其解决方案
    机器学习 Task 03 多层感知机
    机器学习 task2 softmax与分类模型
    异步与闭包与fetch
    baidu API
    my own JSON
  • 原文地址:https://www.cnblogs.com/crazymakercircle/p/10089113.html
Copyright © 2020-2023  润新知