前言
大四上学期,选修课–科技论文写作的期末作业,自选题目。

【摘要】
本文以Java语言为载体,对并发的由来的原因、并发编程会带来什么问题、Java语言中对并发的支持做了简单阐述。致力于对并发方面搭建一个宏观上的知识体系,在并发的概述部分,简单介绍了硬件的发展,以及带来了什么问题,我们又是怎么解决这个问题的,以此引出并发的概念。在设计并发的进一步思考部分,介绍了要做到并发应该要从哪里入手,以及这么做会有什么问题,又该怎么解决。中间还穿插着介绍了Java语言特性的一些必要知识,以便之后了解实现原理的时候能够更好地理解。在怎么保证并发运行部分,仅仅是阐述了并发运行的一些特性,这些特性都是抽象出来的,可以说要保证并发,就是在解决这些问题。在Java语言支持部分,也简单刨析了synchronized、volatile关键字的底层实现,做到知其然更要知其所以然;在这一部分,仅仅只是列出了本人所了解到的方面,在Java语言里,肯定会有一些其他的应用支持,没列出来的原因仅仅是本人能力有限。最后对并发的理解做一个总结。

【关键词】:并发编程、内存模型、锁、synchronized、volatile

目录
1.并发的概述 1
1.1Amdahl定律 1
1.2Gustafson定律 3
2.设计并发的进一步思考 3
2.1硬件发展的问题 3
2.2缓存一致性的问题 5
2.3Java内存模型 5
2.3.1 JVM内存结构、Java对象结构、JMM比较 7
3.怎么保证并发运行 9
4. Java语言对并发的支持 10
4.1Synchronized关键字 10
4.1.1Synchronized可以保证有序性 13
4.1.2Synchronized可以保证原子性 13
4.1.3Synchronized可以保证可见性 14
4.2Volatile关键字 14
4.2.1 Volatile可以保证有序性 14
4.2.2 Volatile可以保证可见性 14
4.2.3 Volatile不能保证原子性 15
4.3Java并发包 15
4.4Fail-fast、fail-safe、COW机制 16
4.5 CAS 16
4.5.1 CAS的问题,ABA问题: 17
5. 结语 17

1.并发的概述
为什么需要有并发?先来看并发是什么,简单点来说,并发就是让计算机能够同时去做多件事情。总的来说,并发是为了获取更好的性能,榨干计算机的硬件资源,能让我们的收益得到最大化;还有一点是因为业务模型的需要,我们只能用并发编程来解决,比如计算机上可以在同一时刻运行多个程序,使得我们既可以一边听歌一边编写代码,当然这需要硬件的支持,比如多核CPU,在操作系统的层面,引入了进程、线程、协程,也是为了是程序能够更好地并发运行。
不难发现,现在并发几乎在现代操作系统中已经是一项必备的功能了,认真想想,其实并发也是现代计算机发展一个必经的阶段。总所周知,现在计算机的计算能力已经非常强大了,但是他的IO操作,或者说对磁盘的读写操作,却是很慢的,在某些情况下,这一个缺点可以让计算强大的优点无法展现出来,可能大部分情况下都会在等待IO操作的结果,时间都浪费在了这上面,如果我们可以在这段时间让CPU也有事情做,也就是说让计算机的计算能力能在这段时间仍然是正在使用状态,那问题就得到了解决,这种手段也正是并发得以出现的主要原因。
我们可以将串行运行的程序改造为并发运行的,因为并发可以提升程序的整体性能,但是具体是怎么提高的,又提高了多少,或者是否是真的提高了?这都是值得我们深思的问题,目前主要有2个定律对这个问题做出回答。
1.1Amdahl定律
Amdalh定律通过系统中并行化与串行化的比重来描述多处理器系统能够获得的运算加速能力。
而所谓运算加速的能力,具体到数字上就是优化前系统的耗时与优化后系统的耗时这个比值。
推导过程:
假设整个系统中,有n个处理器,F是程序中可以并行执行的比例,则串行执行为1-F。T1表示优化前的耗时,Tn表示优化后的耗时,则有如下过程:

所以,我们可以得到S = 1 / (1 - F + F / n)。根据这个公式,如果CPU处理器的数量趋于无穷,那么加速比和程序中并行化执行的比例成正比,和串行化执行的比例成反比。在极端情况下,假设并行处理器的个数是无穷大,那么并行执行的程序耗时接近为0,整体程序的执行时间和处理器的个数关系就不大了。根据这个公式,使用多核CPU对系统进行优化,优化的效果取决于CPU的数量,以及系统中串行化程序的比例。CPU数量越多,串行化比例越少,则优化效果越好,仅提高CPU数量而不降低程序的串行化比例,也无法提高整体系统的性能。
用图示演示的话,就是这个样子:

由此可见,为了提高系统的效率,仅增加CPU处理的数量并不一定能起到有效的作用,需要从根本上修改程序的串行行为,提高系统内并行化的模块比重,在此基础上,合理增加处理器数量,才能以最小的投入,得到最大的加速比。

1.2Gustafson定律
Gustafson定律描述的是系统优化某部件所获得的系统性能的改善程度,取决于该部件被使用的频率,或所占总执行时间的比例。
推导过程:

根据这个公式,我们可以更容易地发现,如果并行化比例很大,那么加速比就是处理器的个数,只要不断地增加处理器,我们就能获得更快的速度。
Amdahl定律和Gustafson定律的结论有所不同,并不是说其中哪个是错误的,知识二者的侧重点不同,也就是这是从不同角度看待问题的结果。Amdahl定律强调,当串行化比例一定时,加速比是有上限的,不管你增加多少个处理器参与运算,都不能突破这个上限;Gustafson定律强大,如果可被并行化的代码所占的比例足够大,那么加速比就能随着CPU处理器的数量增长而增长。
总的来说,如果要提升系统的整体性能,我们需要考虑2个方面:想办法提升系统并行化的比例,同时增加CPU处理器的数量。之后我们所做的事情,主要就是集中在提升串行化比例这方面。

2.设计并发的进一步思考
2.1硬件发展的问题
因为能够让计算机并发执行多个程序,所以可以充分利用计算机处理器的性能,这里的因果关系看起来是顺理成章的,但实际上,他们之间的关系并不只是如此简单。因为绝大多数的程序并不仅仅只有计算任务,还有其他很多任务需要其他硬件来相互配合,比如要和内存交互:读取计算的数据,存储计算的结果等待,这个IO操作是必不可少的,或者说我们无法只通过寄存器来完成所有的计算任务。所以,现在计算机硬件的发展本身就存在一个问题:计算机的存储设备与处理器的运算速度一直以来就有几个数量级的差距,或者说在内存里操作的速度和在处理器里操作的速度一直以来就有几个数量级的差距。为解决这个问题,我们就不得不在内存和处理器之间加入一层读写速度更好的高速缓存:将计算需要使用到的数据复制到缓存中,让计算能够快速进行,当计算结束后再从缓存同步到内存当中,这样处理器就不用等待缓慢的内存读写了。
具体到实现上,按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1)、二级缓存(L2)、三级缓存(L3),技术难度和制造成本相对递减、容量相对递增。
下图是一个单CPU双核的缓存结构:

这里简单分析下单线程、多线程在单核CPU、多核CPU中的影响:
单线程:
CPU核心的缓存只被一个线程访问,缓存独占,不会出现数据访问冲突问题。
单核CPU、多线程:
进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但是由于任何时刻只能有一个线程在执行,因此不会出现数据访问冲突的问题。
多核CPU、多线程:
每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存时,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的缓存中保存一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自缓存的情况,而各自缓存之间的数据就有可能不一样。

2.2缓存一致性的问题
基于高速缓存的存储交互很好地解决了处理器与内存之间的速度矛盾,但是基于上诉情况的分析,也会引入了一个新的问题:就是在多核CPU中,每个核在自己的缓存中,关于同一个数据的缓存内容可能会不一样,也就是说存在缓存一致性问题。
在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享一块内存,当多个处理器的计算任务都涉及到同一块内存区域时,可能会导致每个处理器所缓存的数据出现不一致的情况,如果真的发生这种情况,那数据同步回到主内存的时候,以谁的缓存数据为准就是个问题了。对此,为了解决一致性的问题,需要每个处理器访问缓存的时候,要遵守一些协议,在读写时都要根据协议来进行操作,现在这样的协议主要有:MSI、MESI、MOSI、Synapse、Firefly、Dragon Protocol等等。
除了加入高速缓存外,为了使处理器内部的计算单元能尽量的被充分用起来,就会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结结果和顺序执行的结果是一样的,但是并不会保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么就其顺序性就不能靠代码的先后顺序来保证了。在Java语言中,与处理器的乱序执行优化类似,JVM的即时编译器中也有类似的功能,叫做指令重排序。

2.3Java内存模型
对于C/C++语言,在处理内存时,会直接使用物理硬件和操作系统的内存模型,因此,就会由于不同平台上内存模型的差异,可能程序在一个操作系统上并发执行完全正常,但是到了另一个操作系统上并发执行就会经常出错,所以,我们就必须针对不同的操作系统来编写程序。但是Java语言是有跨平台特性的,JVM自己定义了一套Java内存模型(JMM),在JSR-133[ (https://download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/)]中对JMM给了详细的描述,JMM是一个抽象的概念模型,并不是真实存在的,这里定义了一组规则或者说规范,使得一个线程对共享变量的写入对另一个线程是可见的。以此来屏蔽各种硬件和操作系统的内存访问的差异,让Java程序在各个平台下都能达到一致的内存访问效果。
我们把多个线程间通信的共享内存称为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是一个抽象的概念),其中保存的数据是主内存的数据的拷贝,多个线程在进行通信的过程中,就会出现一系列意想不到的问题,比如可见性、原子性、有序性等问题,而定义的JMM,就是围绕着多线程通信以及与其相关的一系列特性而建立的模型,控制着本地内存和主内存之间的数据交互,同时也定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized、final等关键字、concurrent包等。
为了更好的理解JVM是怎么定义内存模型的,这里和之前说的解决方案做个对比,示意图如下所示:

2.3.1 JVM内存结构、Java对象结构、JMM比较
Java内存模型、JVM内存结构、Java对象模型,这是很容易混淆的3个概念,为了完整性,之前提到了JMM,这里就再简单补充下JVM内存结构和Java对象模型。
2.3.1.1 JVM内存结构
Java代码是要放到JVM上才能运行的,而JVM在执行Java程序时,为了能够更高效、更方便的执行,就会把管理的内存划分成不同的数据区域,不同的数据区域有不同的用途。在Java SE7 版本中,Java虚拟机规范了JVM运行时的内存结构如下图所示:

值得注意的是,
Java虚拟机规范也仅仅是定义了一种规范,具体的实现上,不同的虚拟机可以有不同的实现,但都是会遵守这个规范。
规范中定义的方法区,只是一种概念上的区域、并且说明了这块区域应当具有什么功能,但是并没有规定这个区域是应该处于哪块内存的。
不同版本的JDK,方法区所处的位置不同,上图中划分的仅仅是一种逻辑区域,并不是绝对意义上的物理区域。
运行时常量池用于存放编译期生成的各种字面量和符号应用,但是,Java语言并不要求常量只有在编译期间才能产生。比如在运行时,调用String.intern( ) 方法也会把新的常量放入池中。
堆和栈的数据划分也不是绝对的,比如HotSpot的即使编译器会针对对象分配做相应的优化处理。
除了上图中划分的内存区域外,其实还有一块内存区域可以使用,但这块内存并不由JVM来管理,那就是直接内存,是利用本地方法库直接在堆外申请的内存区域。
2.3.1.2Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的,这个关于Java对象自身的存储模型,就是Java对象模型。
HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示这个类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头、实例数据、对齐填充。
Java对象模型的示意图入下图所示:

3.怎么保证并发运行
原子性:
在一个操作中,CPU不可以在中途中暂停之后然后在调度,即不被中断,就是说要么就执行成功,要么就不执行。
在Java中,为了保证原子性,提供了2个高级的字节码指令monitorenter和monitorexit,这2个字节码指令,对应的关键字其实就是synchronized。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性:
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
Java中的volatile关键字提供了一个功能,就是被其修饰的变量在修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都要从主内存中刷新获取。因此,在Java中可以使用volatile关键字来保证多线程操作变量时的可见性,除此之外,synchronized和final关键字也可以实现可见性。

有序性:
程序执行的顺序时按照代码的先后顺序执行的。
在Java中,可以使用synchronized和volatile关键字来保证多线程之间的操作的有序性。Volatile关键字会禁止指令重排序,synchronized关键字保证了同一时刻只允许一个线程进行操作。
4. Java语言对并发的支持
4.1Synchronized关键字
对程序员来说,synchronized只是个关键字而已,用起来很简单,之所我们可以在处理多线程问题时可以不用考虑太多,是因为这个关键字帮我们屏蔽了很多的底层细节。
Synchronized关键字的用法:可以用来同步方法,也可以用来同步代码块,如下代码所示:

对其class文件进行javap反编译,可以得到:


由此可以得知,
对于同步方法:
JVM采用ACC_SYNCHRONIZED标记来实现同步。
方法的同步时隐士的,同步方法的常量池中会有一个ACC_SYNCHRONIZED标志,当某个线程要访问某个方法时,会检查是否有ACC_SYNCHRONIZED,如果有,则需要先获得监视器锁,然后开始执行方法,方法执行完毕后再释放监视器锁。如果在执行的过程中,有其他线程来请求执行该方***因为无法获得监视器锁而被阻断住。指的注意的是,如果在方法执行的过程中,发生了异常,并且方法内部没有处理该异常,那么异常会被抛到方法外面,之前的监视器锁会自动被释放。

对于同步代码块:
JVM采用monitorenter、monitorexit两个字节码指令来实现同步。
可以把执行monitorenter指令理解为加锁,执行monitorexit指令理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象,该计数器为0,当一个线程获得锁,该计数器自增变为1,当同一个线程再次获得该对象的锁时,计数器再次自增,当同一个线程释放锁,计数器自减,当计数器为0时,锁将被释放,其他线程便可以获得锁。

无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,每个对象都拥有自己的监视锁Monitor,在HotSpot虚拟机中,Monitor是基于C++实现的,抽象成了一个ObjectMonitor类。ObjectMonitor类中提供了几个方法,比如enter、exit、wait、notify、notifyAll等等。
Monitor对象的工作流程,当多个线程同时访问一段同步代码时,首先会进入到Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待,并且当某个线程调用了wait方法后,会释放锁进入Wait Set等待。整个流程的示意图如下图所示:

synchronized加锁的时候,会调用ObjectMonitor的enter方法,解锁的时候,会调用exit方法。
事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁,因为Java的线程是映射到原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,而状态转换需要花费很多的处理器时间,对于代码简单的同步块,状态转换消耗的时间可能比用户代码执行的时间还要长。所以,synchronized是Java语言中一个重量级的操作。而在JDK1.6时,JVM就对锁做出了很多优化,进而出现了适应性自旋锁、锁粗化、锁消除、轻量级锁、偏向锁,这些操作都是为了在线程之间更高效的共享数据,解决竞争问题。

4.1.1Synchronized可以保证有序性
Synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序,只不过由于编译器和处理器都遵循as-if-serial语义,所以我们可以任务这些重排序在单线程内可以忽略。
As-if-serial的意思是,不管怎么重排序,单线程程序的执行结果都不能改变,编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

4.1.2Synchronized可以保证原子性
之前提到过,monitorenter、monitorexit指令,对应的关键字就是synchronized,而通过monitorenter、monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
比如,线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁,即使在执行的过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有的代码都执行完成。所以,这就保证原子性。

4.1.3Synchronized可以保证可见性
被synchronized修饰的代码,在开始执行时会加锁,执行完毕后会解锁。为了解决可见性,JVM定义了一条规则:对一个变量解锁之前,必须先把此变量同步回主内存中,这样解锁后,后续线程就可以访问到被修改的值了。所以,synchronized锁住的对象,其值是具有可见性的。

4.2Volatile关键字
上诉说明synchronized的实现是基于Monitor的,所以,其本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。而volatile是JVM提供的一种轻量级的同步机制,他是基于内存屏障实现的,说到底,他并不是锁,所有他不会有synchronized带来的阻塞和性能损耗的问题。除此之外,volatile还可以禁止指令重排序。
4.2.1 Volatile可以保证有序性
Volatile可以禁止指令重排序,保证了程序会严格按照代码的先后顺序执行,比如被volatile修饰的变量操作有:load、add、save,那么他的执行顺序就是load、add、save,这就保证有序性。
而volatile是通过内存屏障来禁止指令重排的。内存屏障是一类同步屏障指令,是CPU或编译器在对内存随机访问操作中的一个同步点,使得此点之前的所有读写操作都执行后,才可以开始执行此点之后的操作。具体的实现方式,就是在编译期生成字节码时,会在指令序列中增加内存屏障。
所以,volatile通过在volatile变量的操作前后插入内存屏障的方式,来禁止指令重排序,进而保证了多线程情况下对共享变量的有序性。

4.2.2 Volatile可以保证可见性
如果一个变量被volatile修饰,在每次数据发生变化之后,其值都会被强制刷入主存,而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从贮存加载到自己的缓存中 ,这就保证了volatile修饰的变量,其值在多个缓存中是可见的。
其实内存屏障也是实现可见性的一个关键点,因为内存屏障相当于一个数据的同步点,他要保证这个同步点之后的读写操作必须在这个同步点之前的读写操作执行完之后才可以执行,并且遇到内存屏障时,缓存数据会和主内存进行同步、或者把缓存数据写入主内存、或者从主内存中把数据读取到缓存中。所以,内存屏障也是保障可见性的重要手段,操作系统通过内存屏障来保证缓存间的可见性,JVM通过给volatile变量加入内存屏障来保证线程之间的可见性。

4.2.3 Volatile不能保证原子性
线程是CPU调度的基本单位,CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU的使用权。所以,在多线程的场景下,由于时间片在线程间轮换,就会发生原子性问题。而要保证原子性,需要有monitorenter、monitorexit指令的支持,而volatile关键字和这两个指令之间是没有任何关系的,所以,volatile不能保证原子性。

4.3Java并发包
Java.util.concurrent(JUC)中包含了Java并发编程中常用的一些工具类,主要分类如下:
Locks部分:包含在java.util.concurrent.locks包中,提供显式锁(互斥锁、读写锁)相关功能。
Atomic部分:包含在java.util.concurrent.atomic包中,提供了原子变量类相关的功能,式构建非阻塞算法的基础。
Executor部分:散落在java.util.concurrent包中,提供了线程池相关的功能。
Collections部分:散落在java.util.concurrent包中,提供了并发容器相关的功能。
Tools部分:散落在java.util.concurrent包中,提供了同步工具栏,比如信号量、闭锁、栅栏等功能。

4.4Fail-fast、fail-safe、COW机制
Java中的fail-fast机制,默认指的是Java集合中一种错误检测机制。当多个线程对部分集合进行结构上的改变操作时,有可能会触发fail-fast机制,当方法检测到对象的并发修改,但不允许这种修改时,这时候就会抛出ConcurrentModificationException。
Fail-fast可以认为是一种理念,就是在做系统设计的时候,先考虑异常情况,一旦发生异常,就直接停止操作并上报。这样的好处就是可以预先识别一些错误情况,一方面可以避免执行复杂的其他代码,另一方面,这种异常被识别之后也可以针对性地做一些单独处理。
为了避免触发fail-fast机制,导致异常,我们可以使用Java中提供的一些采用了fail-safe机制的集合类,Java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用、并发修改,同时可以在foreach中进行增删操作。这样的集合类在遍历时不是直接在集合本身上进行访问,而是先复制原有的集合内容,在拷贝的集合上进行遍历。这种思想也叫Copy-On-Write,简称COW,是一种用于程序设计中的优化策略,其基本思路是:从一开始大家都共享同一块内容,当某个人想要修改时,就会把真正的内容copy出去形成也给新的容器,然后再去改,改好之后再原容器的引用指向新容器即可,这是一种延时懒惰的策略。这样做的好处是可以对Copy-On-Write容器进行并发的读,当然,这里读到的数据可能不是最新的,因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,而并非是强一致性。总的来说,也可以认为是一种读写分离的思想,读和写再不同的容器。

4.5 CAS
CAS(Campare And Swap),比较并交换,是一种乐观锁的计数,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能够更新成功,其他线程都失败,失败的线程并不会因此而被挂起,而是被告知再这次竞争中失败,会去再次尝试。
CAS操作包含了三个操作数:内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器就会自动将改位置的值更新为新值。否则,处理器不做任何操作。无论哪种情况,他都会在CAS指令之前返回该位置的值,在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值。CAS有效地说明了“我认为位置V应该包含值A,如果包含该值,则将B放到这个位置;否则,不需要改变该位置,只告诉我这个位置现有的值即可。”这其实和乐观锁的冲突检查、数据更新的原理是一样的。而乐观锁,其实是一种思想,相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行更新时,才会正式地对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户自己去决定如何去做。乐观锁是一种思想,而CAS是这种思想的一种具体实现方式。
此外,CAS还有其他的应用。在JVM创建对象的时候,即使仅仅该百年一个指针所指向的位置,在并发情况下也不是线程安全的,可能正在给对象A分配内存空间,指针还没来得及修改,对象B又同时使用了原来指针来分配内存的情况。解决方法就可以 用CAS加上失败重试的方式来保证更新操作的原子性。

4.5.1 CAS的问题,ABA问题:
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。
解决的方法就是给变量加个时间戳,或者加个版本号,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,该变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了AtomicStampedReference来解决ABA问题。

5.结语
并发编程是Java语言的重要特性之一,在Java平台上提供了很多基本的并发功能来辅助开发多线程应用程序。但是,在开发Java 并发程序时,所要面临的挑战之一是:平台提供的各种并发功能与开发人员在程序中需要的并发语义并不匹配,在Java语言中提供了一些底层机制,例如同步和条件等待,在使用这些机制来实现应用级的协议与策略时必须始终保持一致,如果没有这些策略,那在写程序时,虽然程序看似能够顺利地编译和运行,但却总会出现各种奇怪的问题,所以,我们了解甚至熟悉这些底层是怎么实现的就非常有必要。本文仅仅是尝试对这些底层原理的来龙去脉做了一些简单介绍,如想更为体系、更为深入地了解这些原理,还需在编码实践中进一步融会贯通。

参考文献
Java并发编程实战/(美)盖茨(Goetz, B.)等著;童云兰等译. —北京:机械工业出版社,2012.2

深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著. —2版.—北京:机械工业出版社,2013.6