-
目录
序言:在解读线程安全之前,首先要了解Java内存模型,简称JMM,它是由Java语言规范提出来的,主要是描述java语言的某一种特性,该章节主要描述了多线程程序的语义;它包含了,当多个线程修改了共享内存中的值时,应该读取到哪个值得规则。
这些语义没有规定如何执行多线程程序。相反,他们描述了允许多线程程序的合法行为。以上出至<<Java语言规范>>。该定义比较抽象,总结来说就是给大家描述了Java多线程执行的语义,告诉大家多线程是怎么执行的。
一,可见性
在多线程中编程中经常会出现 1.所见非所得 2.无法用肉眼去检测程序的准确性 3.不同的运行平台有不同的表现 4.错误很难重现
有这么一段代码, 在参数为-server 并且是32位JDK的时候 i的值不会打印,64位JDK时也不打印i的值;当参数为-client并且是32位JDK时候 i的值会被打印,64位JDK时则不打印i的值,这就是多线程中所存在的可见性问题,JAVA内存模型就是为了解决这些问题而提出的,最终由JVM去实现。
首先我们分析一下JAVA运行时数据区:
从上图中可以看出 ,当主线程的的isRunning从true变成false时,由于java程序在执行时JVM首先是将类字节码从上到下的解释执行,当进入到while循环时,首先读取的的isRunning值是true,由于是循环(多次循环),JVM中当方法被对次调用或者方法中的循环体多次循环时,JVM就会对调用JIT(运行时编译)编译,由此可见JAVA语言既是解释执行语言又是编译执行语言,这是JIT会进行指令重排和性能优化(PS:只有当进入JIT编译时才会进行),JIT会判断isRunning是否为true,此时的isRunning是为true,那么改值会被缓存起来,只有的所有循环都是直接使用缓存的值,也就是isRunning为true,所以就会出现代码中的问题,子线程无法走到打印i的值那行代码,i的值也就无法被打印(PS:不同的模式情况不同,只有当参数为-client并且是32位JDK时候 i的值会被打印)。
当出现上述的可见性问题时(让一个线程对共享变量的修改,能够及时的被其他线程看到),Java内存模型规定,使用volatile关键字来保证线程的对其他线程的可见。下面是对volatile的定义:
volatile是由JAVA内存模型规范提出,实际由Jvm虚拟机实现。volatile被定义为不可被缓存,这样在上述的案例中isRunning就不会被缓存,当主线程的isRunning被修改为false时,子线程在循环中就可以读取到isRunning为false的值,这样就可以打印出i的值了。另外在程序中被final修饰的变量也可以保证可见性,读取共享对象的final成员变量之前,先要读取共享对象,拿到引用。JDK有一遗留问题:通常被static final修饰的字段,不能被修改。然而System.in,System.out,System.err被static final修饰,却可以修改,必须云溪通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段。
二.线程安全之原子性
原子操作的概念就是资源是否从始至终保持一致,首先看下面的demo:
demo中我们对i的值进行加1操作,一共执行6W次,最后打印的结果却不是我们想象的每次都是6W,而是小于等于6W,说明这个i++操作不是原子性的操作。通过反编译该代码,发现i++操作是由多个字节码指令组成,如图:
当开始多条线程的时候可能对上图中的操作步骤进行割裂,当一个线程还没有执行完成的时候,另一个线程也在同步执行,此时另一个线程也在读取i的值,很有可能跟第一条线程读取到的i的值都是一样的,也就是说他们两条线程最后都对i的值进行了从0到1的改变,虽然i被执行了2次,但是却只增加了1,所以该的原子性没有得到保证,导致了最后的值没有达到我们想得到的结果。具体原理如下图:
那么我们如何实现原子性操作呢?
最简单的方法是通过同步锁,synchronized关键字或者lock接口提供的锁,这样就能保证同一时间只有一条线程在执行,因为线程于线程间是互斥的,但是该操作效率比较低,不适合高并发场景,因为他的并发数只有1,所以一般在并发模式下不推荐该方法。
那么时下常用的一种方法是通过处理器底层提供的Compare and swap比较和交换操作来保证原子性,也就是我们常说的CAS内存操作,CAS操作需要输入两个数值,一个是旧值A,一个是新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。如下图所示:
由于是该操作是由处理器硬件来保证的,所以肯定的是同一时间肯定只有一个操作可以修改成功,保证了原子性。 具体到JDK为我们提供了实现CAS操作的可多线程保证数据原子性的工具类;他们都在J.U.C包下:
以上是Atomic包下的常用类。
当然CAS也不是万能的,它也存在以下3个问题:
1.由于他底层是自旋方式让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2.CAS仅针对单个变量进行操作,不能用于多个变量实现原子操作,即使是多线程,但是同一时间底层处理器也只有一个线程在执行,只是现在处理器速度很快,感觉上像是在同时执行。
3.ABA的问题。
下面重点理解下ABA问题。出现ABA问题的场景大致是这样的,还是回到刚才的demo,如果线程A和线程B同时拿到i的值都为0,但是首先执行CAS操作的是线程A,它执行完成后i的值为1,线程B还没有开始执行,只是拿到了i的旧值为0,原本结果应该为线程B执行CAS操作失败的,但是在B执行前,线程A又执行了一个CAS操作,将i的值从1变成了0,此时线程B才开始执行CAS操作,当线程B判断旧值时候,i的值为0,线程B也成功了,虽然这里都成功了,都改变了变量的值,但是线程A和线程B在拿到同一个版本的i的值得时候却都成功了,这就产生了ABA问题,B修改的i的值已经不是它最初获取到的那个i的值了,他已经经过了从加1到减1的2个版本的变换了,它已经是第三个版本的i了,已经不纯洁了,该问题的原因是在于CAS操作只判断旧值或者引用是否相等,不对这个旧值的版本做判断,才会导致这样的问题,在JDK中为我们提供了2个带有版本的原子操作类AtomicStampedReference:原子更新带有版本号的引用类型 和 AtomicMarkableReference:原子更新带有标记位的引用类型 这两个类中会对所有获取的旧值添加一个版本号,用于在做CAS操作的进行判断,一旦版本号于最初获取的值得版本号不匹配就会失败,这样就很好的解决的ABA问题。
总结:导致线程安全问题的有 原子性 和 可见性。资源争抢问题包括这么两块,一块是线程共享区,共享资源,它是指一个进程内的共享资源,第二个是进程外的资源,比如DB等,
哪些是线程安全的
栈封闭时,不存在线程共享变量,这是线程安全的的。
局部对象引用本身不共享,但是引用对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其他线程可见,那么也是线程安全的。
不可变对象(只提供get方法不提供set方法),也是线程安全的。
使用Threadlocal时,相当于不同的线程操作的是不同的资源,也不存在线程安全问题。