既然cpu同一时间只能执行一个线程,为什么还会出现并发问题

假设两个请求同时请求controller,进行减库存操作,在数据库中判断库存>=0才进行减库存,这时候不是不存在两个线程同时进行update操作为什么库存很多时候还是会小于0

你没理解并发,方法问题是线程之间的问题,跟cpu没有关系,但cpu也可以使用多线程,只是单cpu进行多线程并不会提高执行速度,甚至还会降低速度
,因为有线程上下文的切换开销,多cpu下的多线程才会翻倍提高。
那并发问题为什么会出现,更cpu没关系,而是跟线程切换有关,举例如下

if(i ==1) {
//执行点1
i++;
System.out.pint(i)
}//执行点2
假如线程A和线程B同时执行上面的代码,当线程A到达执行点2时,cpu进行时间片到期会切换到线程B,去执行线程B,此时线程B执行到If语句
发现i不等于1,则不会调用system。out这是正常的。
但是:如果现程A到达执行点1时时间片到期,于是i++还没有执行,此时线程B执行if语句发现条件为true并调用了system。out。当线程B执行完后
线程A继续执行点1后继续执行,于是system.out被执行了两次,这就是并发问题。所有并发问题与cpu无关,与线程切换有关

即使你只有一个cpu,假设只有一个核心,只能同一执行时间只能执行一个线程,那么如果你的程序不能同步操作敏感资源,那么仍然会出现并发问题,因为cpu会根据虚拟机或者操作系统的调度,交错地执行多个线程,由于cpu在从一个线程切换到另一个线程时,断点在哪里,程序是无法直接控制的,因此效果就像多个线程并发一样。因为并不是一个线程执行完毕再执行别的线程。这就是多任务调度。

就以两只手作为比较,桌面上只有一颗糖,这时候你的两只手都知道桌面上只有一颗糖,当你的左手拿到糖后突然还未进行库存的减操作,让出资源,当再次进入的时候,不幸的是右手先进入了,这时候右手依然认为桌面上有糖的存在,这就导致了问题的发生。

如果是库存的问题,最好是写到一条sql中,这样确保数据库执行时会有排他锁,比如:
update table set data = case when data > 0 then data - 1 else data end
但是如果业务逻辑复杂的话,需要通过代码保证

因为一个cpu有多个核啊

一个cpu可以有多个核心 ,一个核心也可以超线程。先运行1+1 再运行 2+2 和同时 1+1 和 2+2 结果都是一样的 但是时间明显是后者短

线程的确不是越多越好,Google给出的建议是,线程数=cpu(核心数)+1,也就是说假如是单核,可以并发2个线程,3核4个,如此类推
另外假如一个controller执行下面一段代码

void method (){
    sum = sum -1;
}

假如a,b两个人同时访问这个方法,假如a先进入方法,改变的sum的值,但是,这时候jvm没有来得及同步a改变之后的值,这时候b进来了,呢么b是不是拿到还是sum原来的值而不是-1之后的值是不是就出现问题了?所以,并发问题其并不一定是否关心核心数,其关心jvm处理逻辑,jvm在使用某一个值得时候会先将其假如缓存区(也好像叫交换区)具体忘了,修改值之后并不会第一时间同步,所以就会出现值和预期不一样,当然其也有解决办法,给你一篇我曾经写的博客https://blog.csdn.net/zhangpan_soft/article/details/52415238

再补充一点,比如,单例,我们见得很多的都是双重同步锁,然而许多人忽略一点就是,真正的双重同步锁,在类声明变量的时候,会加入volite关键字,其目的为了保证其可见性!因为若是没有保证科技性,完全有可能,创建了实例之后,没有来得及同步数据,而锁已经释放,这时候就会出现多实例了,所以真正的双重校验锁应该这样写

     public class Singleton{
            private static volatile Singleton instance;

            publlic static Singleton getInstance(){
                这里面写双重校验锁
            }
     }

注意我在声明的时候用了valatile关键字,其就会保证可见性,对于多线程里有两点至关重要,一:原子性,二:可见性,所以对于多线程,很多时候,不是加几个读写锁了,之类的就可以避免并发问题,因为只是解决了原子性,而没有解决可见性,两者缺一不可

多线程在实际生产环境中是个很复杂的东西,光看理论是不行的,就像上面所说的,原子性和可见性这两点是多线程必须要考虑的两点!!