java并发的学习笔记(包含数据库多事务的并发)

             修订次数:2

             我知道这个主题对我一个新人菜鸟有点太过大了,但是最近看了很多资料,后面有参考资料的出处。想总结一下,提升自己,所以打算在以后有了新的想法以及知识储备后,不断修订这篇文章,也希望大家可以对我不足的地方予以指导。

            了解到这个问题是从《java编程思想》这本书,在这里不得不推荐这本书一下,真的很经典,可以说是让我真正的走进了java的世界。

             为什么会产生并发

             一

            同一时刻,大量异步请求,就我所接触的,我们平时所用到的页面获取动态数据的方式一般都是通过Ajax向服务器发出请求,Ajax这个请求默认的请求方式为异步,它有一个属性 Asynchronized:默认为true.也就是当它为true的时候,浏览器运行到这里会开辟一条新的线程去处理这个请求,如果为false,那么浏览器运行到了这里,会等待请求返回,然后再做其他事情,所以这里是阻塞的,如果访问量大的话,会造成大量用户都在等待。

            值得一提的是js是单线程,那么XMLHttpRequest在连接后是否真的异步? 
其实请求确实是异步的,这请求是由浏览器新开一个线程请求。当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更 事件放到 JavaScript引擎的事件处理队列中等待处理。当浏览器空闲的时候出队列任务被处理,JavaScript引擎始终是单线程运行回调函数。 javascript引擎确实是单线程处理它的任务队列,能理解成就是普通函数和回调函数构成的队列。

            二         

            多事务,我们知道事务有四个特性ACID(Atomicity原子性,Consistency一致性,Isolation隔离性,Durability持久性),事务可以帮我们解决很多问题,但同时因为并发的原因,也带来了很多问题,比如丢失更新,脏读,不可重复读,幻读,

           三  

            多线程,其实并发归根揭底,我们都可以往多线程身上去推,嘻嘻,感觉多线程是爹呀,言归正传,说到多线程又想到多进程,这里延申一下,进程是资源分配的最小单位,线程是程序执行的最小单位,我们平时可能会用到资源管理器,那里面的一条一条记录,就是一个一个进程。进程有自己独立的地址空间,也就是每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,进程创建完后,线程就轻松多了,进程是爹,已经为他们打好江山,但是进程这个爹害怕儿子争夺皇位,就与时俱进提出共享计划,就是说老爹我的资源是你们每一个人的,不用争,你们要和和睦睦,想法虽好,但是你生那么多儿子干嘛,这个上厕所,那个就要排队,兄弟们越来越多,抱怨声也越来越多。

扫描二维码关注公众号,回复: 3196858 查看本文章

            回归正题,其实理解多线程是从一门课上理解的,这门课是《操作系统》,里面学到了一个概念,叫时间片,这个概念对理解多线程太好了,可以这样理解cpu的时间被分成一片片,然后分配给好多线程,让后cpu就一个时间片一个时间片的运行,因为cpu运算速度太快了,所以我们产生了幻觉,cpu一直在为我们当前正操作的这个程序服务(当然这是针对单核cpu来说),和远古的分时系统异曲同工之妙。

          四

          多核cpu的cpu的诞生,可谓真正意义上的实现了并发,但是这又为并发所带来了问题,加了点料

          如何处理并发所产生的问题 

            一

           就第一点而言,这并不是问题,而是实实在在的需求,因为我们设计一款产品的话,最应该看重的就是用户体验。

            二

           互联网中存在着抢购,秒杀等高并发场景,使得数据库在一个多事务的环境中运行,多个事务的并发会产生一系列问题,主要问题之一是丢失更新,一般而言存在两种丢失更新

           假设一个场景,一对夫妻共用同一个账户

第一类丢失更新
时刻 事务一(宫先生) 事务二(宫太太)
T1 查询余额10000元  
T2   查询余额10000元
T3   网购口红1000元
T4 请石龙吃饭消费1000元  
T5 提交事务成功,余额9000元  
T6   取消购买,回滚事务,余额10000元

        这两个事务并发,一个提交,一个回滚,造成了数据不一致,称其为第一类丢失更新,不过大部分数据库已经解决了这类丢失更新,解决方法是对行加锁,这个加锁有两种,即乐观锁和悲观锁

       乐观锁和悲观锁并不是真正意义上的锁,只是人们在解决丢失更新问题时的不同解决方案,体现人们看待事务的态度,和他们的名字一样,一种持悲观态度,一种持乐观态度。

      悲观锁,认为每一条sql语句都会出现跟新丢失问题,是一种利用数据库内部机制提供锁(排他锁,在sql语句后加 for update)的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能对数据库进行更新了,这就是悲观锁,如果数据库查询的数据较多,更新的数据较少,效率就会很低。

      乐观锁,这货天生乐观,认为每一条sql语句都不会出现丢失更新问题

      乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,他的设计里面由于不会阻塞其他线程,所以不会引起线程的频繁挂起和恢复,这样能够提高并发能力。乐观锁使用的是CAS原理,何为CAS?

     其实很简单,我这篇文章就用到了,请注意我文章的开头有个修订版本号,CAS也是加个版本号,假如初始值是version=0,事务一进来了,先保存这个0,然后读取共享资源10000元,然后花掉1000,接着比较这个0是否和version一样,如果一样就把余额9000更新,然后version++,如果不一样说明 哎,他知道有人已经动了这笔钱,就放弃更新余额,可以考虑放弃,或者重试,这样就是一个可重入锁。是不是很巧妙,但是会引发一个问题,就是ABA问题,何为ABA问题?

       其实大家已经不需要知道了,因为这个版本号已经解决了ABA问题,出现ABA问题的设计思路是把共享变量当版本号,每次保存它的旧值,产生更新操作前进行比较。还是画个表吧

ABA
时刻 线程一 线程二
T0 读入x=A  
T1   读入x=A
T2   x=B
T3 执行C=x-3  
T4   x=A
T5    
T6 判断因为x=A,所以更新数据 D=A+3
T7   因为x=A,所以更新数据

        相信大家已经发现问题了吧,其实这个线程二就是个骗子,故意挖坑让线程一往里面跳,在T2时刻改变了x的值,这线程一也是不懂事,直接就操作起来了,没办法,人家的定位就是天生乐观,这线程二狡猾呀,在线程一操作完之后,又把x的值改回去,线程一还傻愣愣的判断x==A,为true,仰天大笑,no problem!殊不知自己的A级跑车被换成了B级的(来自宫先生的偷笑)

         第一类丢失更新终于解决了,第二类丢失跟新其实很简单,继续耐心的画个表

第二类丢失更新
时刻 事务一(宫先生) 事务二(宫太太)
T1 查询余额10000元  
T2   查询余额10000元
T3 请老黄吃饭消费1000元  
T4   网购包包1000元
T5 提交事务成功,查询余额10000元,消费1000元,余额9000元  
T6   提交事务成功,查询余额10000元,消费1000元,余额9000元

            由于不同事务之间无法探知其他事务的操作,导致宫先生请老黄吃的饭没花钱(美滋滋),但是银行不乐意了,客户不满意,公司就要出方案呀,于是数据库的隔离级别诞生了,

            第一级别脏读(DIRTYREAD),允许不同事务之间,探知操作,但是又产生新问题了

脏读
时刻 事务一 事务二 备注
T1 查询余额10000元(宫先生) 宫太太  
T2   查询余额10000元  
T3   网购香水1000元,余额9000元  
T4 请创创吃饭1000元,余额8000元   读取到事务二,未提交余额为9000元,所以余额为8000元
T5 提交事务   余额为8000元
T6   回滚事务 由于丢失更新已经克服,所以余额为8000元

            分析上表可知错误的根源在于T4时刻,事务一读取到了事务二未提交的数据,这样的场景被称为脏读,为了克服脏读,公司又提出了第二个隔离级别,读写提交(READ COMMIT),克服了脏读,但是也出问题了

  

时刻 事务一(宫先生) 事务二(宫太太) 备注
T1 查询余额10000元    
T2   查询余额10000元  
T3   网购辣条1000元,余额 9000元  
T4 请斌斌吃法2000元,余额8000元   由于采取了读写提交,宫先生无法读取太太未提交的余额9000元
T5   继续买买买8000元,余额1000元 由于采取了读写提交,宫太太无法读取先生未提交的余额8000元
T6   提交事务,余额1000元 宫太太提交事务,余额未1000元
T7 提交事务发现余额1000元,不足以买单,于是这顿饭彬彬请了。    

        

   

     

                  由于,宫太太的手太快,先一步提交事务,导致宫先生没钱结账,让彬彬请了,彬彬认为这对夫妻串通好的,

宫先生觉得十分冤枉,就又向银行投诉了,银行也很负责,找到了甲文骨公司,甲文骨公司紧急召集技术人员连夜开会,

提出了第三种隔离级别,可重复读(REPEATABLEREAD),因为上述问题的根源是余额不可以被重复读取,但是。。。。

看图

时刻 事务一(宫先生) 事务二(宫太太) 备注
T1   查询消费记录10条,准备打印  
T2 宫先生和凯凯去网吧上网消费100元    
T3 提交事务    
T4   打印结果11条  

              因为消费记录是可重复读取的,宫先生的消费记录被宫太太打印出来了,宫太太很奇怪,觉得这打印机出现了幻觉,我什么时候去上网了,于是。。甲文骨公司连夜开会,提出了数据库第四种隔离级别,序列化

           序列化是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

           这个还不是很了解,等了解后再补充

           好了,多事务的讨论就到此为止了,忽然想到大三数据库老师曾经布置一个作业,让我们每个人写一篇关于事务的论文,当时潦草应付,有句话是对的,出来混的,迟早都要还的(手动滑稽),刚写完,又和大佬交流了一下,忽然发现了一个严重的弊端,我这分析都是建立但单数据库,单服务上的,但现在大多是多数据库,多服务,所以应该去研究一下分布式事务,后续再补充,

            分布式事务的讨论

@Transaction            

 订单接口A{

                       下单

                       库存服务.减库存

}

             如果上面的接口在减库存的地方出现了超时异常,即减库存的请求发出去了,却因为网络原因,没有收到返回值。但是接口A认为发生了异常,进行回滚,所以下单失败,但是减库存只是没有返回成功的信号,其实并没有异常,所以并没有回滚,最后结果是下单失败,库存却减少了,这显然是不符合需求的。

             来看两个重要的理论

            CAP理论

            CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

  • 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
  • 可用性(Availability) : 每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成   

            BASE理论

  • 基本可用性(Basically Available):当系统出现了不可预知的故障,但是相对于正常系统而言还是可用的
  • 软状态(Soft State):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时
  • 最终一致性(Eventually Consistent):系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值

三&四

           因为现在多是多核cpu,所以三四可以放一起讲,并发问题存在已久,但是IT界风起云涌,人才辈出,一个又一个大佬提出了一个又一个优秀的方案

          在cpu多核的情况下,每个处理器都有自己的缓存区,为了保证多核处理器数据的一致性,引入了多处理器的缓存一致性的协议MESI、MOSI、Firefly等,比如Intel的MESI,每个cpu都会把自己需要的数据从主内存中拷贝出一个副本放在自己的高速缓冲区里,当某一个cpu对一个共享变量(其他cpu也有这个变量的副本)执行写操作的时候,会发出信号把其他cpu缓存中的变量置为无效,当其他cpu读取这个变量时发现其无效,那么就需要从主内存重新获取,当然这个获取是在写操作之后

             理解了上面的工作会对熟悉java的内存模型有很大的帮助,先上图     

                java内存模型规定了所有变量都存储再主内存,每条线程都有自己的工作内存,操作过程中,如果需要的变量工作内中没有,就去主内存中拷贝一个副本,不同线程间不能访问对方的工作内存,之间的交互操作都在主内存中发生。同缓存一致协议一样,java内存模型也定义了8中操作规则来定义工作内存和主内存的交互协议。

                

- lock(锁定):作用于主内存的变量,锁定一个主内存中的变量,标志该变量为一条线程独占的状态。

- unlock(解锁):作用于主内存的变量,清除变量的独占状态,只有清除了独占状态,其他线程才能lock 该变量。

- read: 作用于主内存的变量,把主内存的一个变量的值从主内存传输到线程的工作内存,以便后来的load操作使用。

- load: 作用于工作内存的变量,把read回来的值放入工作内存的变量副本中。

- use: 把变量传给执行引擎使用。

- assign: 把从执行引擎接收到的值赋值给工作内存中的变量。

- store :作用于工作内存的变量,把一个变量的值传送到主内存,以便后面的write 使用。

- write : 作用于主内存的变量,把store的变量放入到主内存中的变量。

                 需要注意的是虚拟机会保证上述操作都是原子操作

  并发编程有三个很重要的概念:原子性,可见性和有序性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可以这样理解,如果一个操作是不可分的,就是它只需要一步就能完成,那么他就是原子性的

例如 int i=0;

反例i++;其实它是两步 原式为i=i+1;第一步计算等号右边的值,第二步把右边的值赋给等号左边。

当然,如果你能保证这两步要么都成功,一个失败就算都失败,那么也是原子操作,

抽象出来就是 电路中的串联,一个开关的话,要么亮,要么不亮,两个开关,三个开关......

          没有美术细胞,见谅哈

          另外在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

           可见性 是指共享数据的时候,一个线程修改了数据,其他线程知道数据被修改,会重新读取最新的主存数据,是不是很像上面那个Intel的MES协议,是的,这叫英雄所见略同,java对此有一个关键字volatile,另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

           有序性????,难道程序执行不是从上到下顺序执行的吗?确实如此

            

  在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

  在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

  另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。(这个happens-before让我想到了操作系统和离散数学提到的有向图,是不是很像嘻嘻)

  下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  这8条原则摘自《深入理解Java虚拟机》。

  这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

  下面我们来解释一下前4条规则:

  对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

  第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

  第三条规则是一条比较重要的规则,直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。(这个我在上面已经提到了)

  第四条规则实际上就是体现happens-before原则具备传递性。

volatile的原理和实现机制

  前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

      对于第一点,可以这样理解,把整个模块分三块,ABC,A时内存屏障前,B时内存屏障,C时内存屏障后,ABC这个顺序不能改变,至于AC里面的指令是可以重排的

       终于写完了,能力有限,希望大家指出错误。

 参考资料

         《java编程思想》

         《javaEE轻量级框架整合》

         《深入理解Java虚拟机》

           https://www.cnblogs.com/dolphin0520/p/3920373.html

           https://blog.csdn.net/wlittlefive/article/details/79434657

           

        

猜你喜欢

转载自blog.csdn.net/qq_33543634/article/details/82145729
今日推荐