什么是高并发(java为例)

当提到高并发的时候,很多人就有疑问,到底什么是高并发编程?

以登录功能为例。当登录的时候,是用户拿用户名,密码到数据库里访问是否存在,存在则跳转到登录页面。然后修改访问次数为+1.否则跳转到失败页面,访问次数不加1.

当一个用户进行访问的时候,是不存在并发性问题的。因为用户查询库表,修改访问次数,不会受到别人的影响。

但是当两个用户访问的时候,在查询库表的时候,假定两个用户是顺序的。第一个登陆进来,完成了修改登陆次数的操作之后,第二个用户才登陆进来。那么这个也是不存在并发性问题的。

那什么时候出现的并发性问题呢?当有很多个用户同时登陆,假定恰好一个用户刚查询到自己的账号和密码,然后把登陆次数从库表读到了内存中,还没来得及该表,就是说还没来得及修改登陆次数,这个时候又来了一个用户查询自己的账号和密码,虽然后面的账号来的晚,但由于存在表中的顺序靠前,查询的块,接下来去表里取登陆次数早于第一个用户提交结果到表(实际上和第一个用户获取到的登陆次数是一样的,因为第一个用户还没来得及修改表里的这条记录),然后再到库表中修改自己的登陆次数。这个时候,当第一个用户再修改登陆次数的时候,由于是基于自己读取到的登陆次数进行加1的操作,就会丢失掉第二个用户的登陆次数。实际上两个用户虽然都登陆了,但实际上只记录了一个用户的登陆次数(丢失修改)。如果同时登陆的用户数非常多(例如一毫秒1个(一次数据库操作需要几个毫秒)),就会出现很多这类问题。

上面是并发造成的问题之一,是数据安全性问题。究其原因是什么呢?是因为登陆次数是很多个用户共享的,而且是共享修改和读取的。为什么同样高并发的用户登陆,对于登陆的账号不会出现安全性问题呢,因为不涉及修改操作,也不涉及共享数据(查询的不是同一条记录,且结果不相互影响)。因此可以这么下结论:只有出现共享数据的问题才会有并发的数据安全性问题。

当然,并发的数据安全性问题不仅仅局限于上述的场景,还会包含,诸如脏读等问题。

那么我把登陆次数的访问和修改加锁,是否可以完全解决并发性的数据安全性问题呢?通过一个锁来控制对登陆次数的修改,一次只能一个用户修改,直到修改完登陆次数,释放锁,才允许下个用户访问。

这完。但又有一个新的全可以解决一部分安全性问题问题,锁也会成为多个线程的共享数据,既然锁是共享数据,也不可避免的出现了并发修改的问题,那再对锁加锁的话,显然就进入极限但不收敛的状态了。不是100%可靠。

很多人不理解,锁为什么也会导致并发不安全?锁读的时候会进行判断的啊,为何还不行?这里就有一个很隐晦的问题了:指令优化。

正常情况下,我们代码顺序执行,先判断锁的状态,是否锁住了。然后如果没有锁住,则进入,否则持续间隔时间访问锁的状态,直到锁被其他线程释放,然后才进入。按理说不存在像从数据库表里读记录到内存,会出现时间差的问题,导致顺序出现差异,为何还会有并发性问题呢?

这里得讲一个原理:jvm指令优化。

我们知道,.java后缀的源代码要被java虚拟机执行,需要进行编译成 .class 后缀的文件。那这个class文件要被虚拟机执行,实际上里面的代码指令,不再是我们写的那些 public,static main String int等关键字了。会被转换成 另外的一个指令集(如load,read,reload等)这个指令集的转换,是编译器进行的。我们直观的理解,编译器会按照顺序编译,即编译器会根据某个顺序将我们的一行java代码转换成class后缀的文件。可实际上并非如此。实际上编译器有编译语法,也有优化语法。会根据具体场景做一些执行顺序上的优化。这些顺序上的优化,可能是将两次相邻的读一个数据的操作合并为一个语句进行执行(单线程情况下,编译器判断为指令重复,将两条对某个数据读操作相邻(可能中间含有其他数据的其他操作,但也认为是相邻)进行优化成了一条读指令)。但在并发情况下,这种合并会出现诸如上面两次读取,中间另外一个线程修改数据而导致结果不一样的情况。是不能进行简化成一次读取的。所以就出现了,优化后的语句,执行在并发情况下,是和顺序执行的顺序不一致的结果。所以我们说,是由于jvm指令编译的优化,导致了锁并发的失效。

如果你读过java并发编程的艺术一书,可能知道这个时候应该用volatile关键字修饰锁,不允许jvm优化。这总可以解决并发问题了吧。

那是否就能万无一失了呢?答案告诉你,还是否定的。这是为什么呢?

再讲一个原理:汇编指令优化

我们知道jvm是操作系统层面的执行指令集。实际上,我们执行指令最终是硬件的电气特性。这个电器特性在执行的过程中,只认一个东西,就是01.指令。那么我们jvm层面的class文件对应的指令集(诸如load,read等)是如何被运行的呢?答案是,先有编译器,转换成汇编指令,再由编译器转换成二进制指令。

刚才我们说了,volatile可以禁止掉jmv编译时进行优化,那汇编的过程中,实际上也是有类似的相同的问题的,也是会进行相似的优化指令执行的顺序的。当合并两次读操作的时候,同样在另外一个线程在两次读操作之间进行了写入(这里的是class指令的read,load),会导致两次结果不一致的情况。这时,虽然加了volatile关键字。或者使用的是原子变量(也是禁止编译时指令优化),也都是不够线程安全的。

那么怎么办呢?并发编程的艺术上,对该问题提出了ABA的模型。以及对应的解决范式。具体本文不详细写。需要的同学私信获取答案。

为什么ABA的模型可以解决并发情况下多线程的数据安全性问题呢?是因为它避免了在汇编过程中进行指令优化时带来的执行顺序的异常。

上面讲了并发性的数据安全性问题。

那么是否高并发编程解决了并发的多线程的数据安全性问题是否就解决了高并发的问题呢?

答案还是 不是。

高并发除了数据安全性问题。还有一个层面的问题:资源瓶颈。

这又是为什么呢?当一个用户登陆的时候,读取的是一个用户的信息到内存,进行比较。

假设内存1g,一个用户信息1M,那么在内存里面最多同时可以有用户信息1024条(不考虑其他程序占用内存的情况)。假设登陆在一万个用户来的时候,对数据库的压力导致访问时间延长为1s,那么在一秒的时间内,这个1g内存只能供给1024个用户进行登陆。来了一万个用户,就需要将近10s的时间,内存才会完全的处理完毕登陆操作。因此,如果其他资源够用的情况下,超过1024个用户同时登陆,必然会出现内存是瓶颈的问题。(可以按照该方式进行计算内存的负载能力)。那实际情况下,并非完全如此。因为当内存使用量达到一定比例的时候,可能会触发与硬盘的缓存的交互(涉及到调度算法)。如果在高位频繁出现或持续在高位导致频繁调度,就会对cpu造成压力(调度算法是计算密集型任务,比较耗cpu资源),严重可能直接导致cpu使用率100%出现死机的状况。这个时候,资源瓶颈就转换到cpu。

再换个场景,比如是下载资源或者上传资源文件的接口,这个很好理解,当很多用户同时上传的时候,网络带宽有限,会挤爆网络,导致上传失败或者下载失败。这个情况下高并发的资源瓶颈在网络。

其他硬件都可能因为高并发的功能不一样导致资源瓶颈。那我们就说,高并发实际上也是资源瓶颈问题。

如果我们硬件很给力,完全够用,是否高并发问题就好了呢?

这里其实还有一个层面:软件程序编写。

如果程序在多次调用不释放资源的情况下,也是会造成虽然访问不是在同一时刻,仍然可能出现资源耗尽的问题。那可能就是上个时间段占用的资源(例如map占用了内存)在很长时间内无法释放。导致虽然不是同一时刻访问的多个线程,也会出现资源耗尽的情况。这也算是高并发的一个方面。所以,写代码的时候,要对代码质量进行把控,也有个词,叫幂等性。

基于以上讲解,高并发编程实际上主要解决以上几个方面:

1.共享数据的安全性问题

2.共享资源的瓶颈问题

3.共享资源的使用性问题

解决了以上三个大问题,并发编程和其他的编程,也就不存在考虑不到的死角问题了。高并发并不可怕,掌握以上三大方面,高并发也只不过是找到一个上限值的问题了。

 

 

 

你可能感兴趣的:(java,并发编程)