Java多线程与并发编程 | 什么是并发

229 阅读3分钟

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

前言

不知道你是否有过这样的经历,明明本地测试没有问题,为什么一到线上就出问题了?为什么线上的数据出现偏差,但是代码都没问题,用测试账号也复现不了,最后只能采用极不靠谱的解决方案,手动修改数据库,改掉异常数据,并且线上也没有出现问题了,但是隔十天半个月后的某一天又出现了,故技重施。这种做法是不可取的,如果确实因为异常数据影响了主要流程,我们可以采用这种应急方式,但是事后我们应该也必须定位到问题所在。

什么是并发

  • 并发就是指一个程序同时处理多分任务
  • 并发编程的根源在于对多任务情况下对访问资源的有效控制

例子

之前开发过一个需求,记录一下资源的下载次数,需求很简单,数据库加个字段呗,下载一次数值加1,要不到半个小时就搞定了,然后简单测试了一下,没问题,提交代码,这时我根本没有意识到可能会有并发问题。直到我学习了并发编程后,回想起来之前写的这个需求好像有点问题,我在本地模拟了一下高并发的情况下。确实如我猜想的一样。看一下下面的代码。

public class DownloadsSample {
        public static int users = 100;//同时模拟的并发访问用户数量
        public static int downTotal = 50000; //用户下载的真实总数
        public static int count = 0 ;//计数器
        
        public static void main(String[] args) {
            //调度器,JDK1.5后提供的concurrent包对于并发的支持
            ExecutorService executorService  = Executors.newCachedThreadPool();
            //信号量,用于模拟并发的人数
            final Semaphore semaphore = new Semaphore(users);
            for(int i = 0 ; i < downTotal ; i++){
                executorService.execute(()->{
                    //通过多线程模拟N个用户并发访问并下载
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
            try {//保证上面的线程能够走完(一定要加这个)
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.shutdown();//关闭调度服务
            System.out.println("下载总数:" + count);
        }
        
        public static void add(){
            count++;
        }
}

上面代码,模拟了100个人去下载,下载一次计数器加1,运行后打印结果

下载总数:49118

再次运行

下载总数:50000

多测几遍,发现每次结果都不一样,并且大多数情况下比目标50000要小。

这也证实了我之前的猜想,庆幸的是我那个需求并不要求精确的下载量,一点点偏差并不会影响主要流程。

分析

image.png

当只有一个用户操作时,拿到最新的count,然后+1,再存回数据库,一切都按照正常顺序来,并不会发生异常情况。 image.png 当有多个用户一起操作时,一起去拿count,拿到的count都是1,然后一起改成2,再存回数据库,这其实就等于漏加了一次,最终会导致总次数变小。

优化

既然知道了原因,那么我们就可以针对这个进行修改,既然时多线程一起去拿,那么最简单的,我们控制这个add()方法,一次只让一个线程来拿,操作完下一个才能继续操作。

/*线程安全*/
public synchronized static void add(){
    count++;
}

最简单的方式,直接加把锁,测试一下,结果ok

总结

这里虽然已经解决了这个bug,但是处理方式过于暴力,比较影响性能,后面还会介绍其它优化方式,这里不展开,并发问题与我们实际开发是紧密联系的,没有人会告诉你这里要考虑一下,那里需要注意一下,我们应该时刻绷紧这根筋,有意识的去注意写的代码是否会出现并发问题。