时间:2017-6-15来源:本站原创作者:佚名
Java内存模型与线程安全的本质

1主内存与工作内存

java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量和java编程中所属说的变量有所区别,它包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然不会存在竞争问题。

java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,线程间变量值的传递均需要通过主内存来完成。

拷贝副本:一个对象的引用、对象中某个在线程访问到的字段是有可能存在拷贝的,但不会有虚拟机实现成把整个对象拷贝一次。

主内存主要对应于java堆中的对象实例数据部分,工作内存则对应虚拟机栈中的部分区域。

2原子性、可见性与有序性

java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。

1、原子性

java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write。我们大致可以认为基本数据类型的访问读写是具备原子性的(除long和double)。

更大范围的原子性保证是同步块——synchronized关键字。

2、可见性

可见性是指当一个线程修改了共享变量,其他线程能够立即得知这个修改。

java实现可见性的关键字:

(1)volatile:volatile的特殊规则保证新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

(2)synchronized:同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的。

(3)final:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半的对象”),那在其他线程中就能看见final字段的值。

3、有序性

java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

java语言提供了一下关键字来保证线程之间操作的有序性。

volatile:此关键字本身就包含了禁止指令重排序的语义。

synchronized:是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

从以上3中并发的特性可知,synchronized关键字在需要这3中特性的时候都可以作为其中一分钟的解决方案。越“万能”的并发控制,通常会伴随着越大的性能影响。第13章讲解虚拟机锁优化。

3先行发生原则

如果java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很繁琐,但是我们在编写java并发代码的时候并没有感觉到这一点,这是因为java语言中有一个“先行发生”的原则。这个原则非常重要,他是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。

先行发生是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

java内存模型有一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来,他们就没有顺序性保障,虚拟机可以对他们随意地进行重排序。

(1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

(2)管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调地是同一个锁,而“后面”是指时间上的先后顺序。

(3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

(4)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

(5)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

(6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

(7)对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

(8)传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要收到时间顺序的干扰,一切必须以先行发生原则为准。

4java语言中的线程安全

我们讨论的线程安全,限定于多个线程之间存在共享数据访问这个前提,因为一段代码如果根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。

为了深入理解线程安全,可以将java语言中各种共享数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1、不可变

不可变的对象一定是线程安全的。保证对象行为不影响自己状态的途径有很多,最简单的是把对象中带有的状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。

2、绝对线程安全

在javaAPI中标注自己是线程安全的类,大多数都不是绝对的线程安全。需要在方法调用端做额外的同步措施。

3、相对线程安全

此级别是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。大多数线程安全类属于此类型,如Vector、HashTable、Collection的synchronizedCollection()方法包装的集合等。

4、线程兼容

是指对象本身并不是线程安全的,但是可以在调用端正确地使用同步手段来保证对象在并发环境下安全地使用,我们平常说一个类不是线程安全的,绝大多数是这种情况。javaAPI中大部分类都是线程兼容的,如与前面Vector和HashTable对应的ArrayList和HashMap等。

5、线程对立

是指无论调用端是否采取同步措施,都无法在多线程环境下并发使用的代码。很少见。

5线程安全的实现方法

1、互斥同步

互斥是实现同步的一种手段,临界区、互斥量、和信号量都是主要的互斥实现方式。互斥是方法,同步是目的。

实现方法:

(1)synchronized;

(2)java.util.concurrent包中的重入锁ReentrantLock,需要lock()和unlock()方法配合try/finally语句块来完成。相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁、锁可以绑定多个条件。

2、非阻塞同步

互斥同步最主要的问题是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

从处理问题的方式来说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要加锁、用户态核心态转换、维持锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略。通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,知道成功为止),这种乐观的并发策略的许多实现不需要把线程挂起,因此这种同步操作成为非阻塞同步。

3、无同步方案

要保证线程安全,并不是一定就要进行同步,同步只是保证共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那就无须任何同步来保证正确性,因此会有一些代码天生就是线程安全的。

可重入代码:相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全。有一些共同特征,例如不依赖存储在堆上的数据和公用的资源、用到的状态变量都由参数中传入、不调用非可重入的方法等。可以通过一个简单的原则来判断代码是否具备可重入:如果一个方法,它的结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也是线程安全的。

线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。大部分消费队列的架构模式都会将消费过程尽量在一个线程中消费完成,其中最重要的一个应用实例就是Web交互模型中的“一个请求对应一个服务器线程”的处理方式,很多web服务器应用都可以使用线程本地存储来解决线程安全的问题。也可以用java.lang.Threadlocal类来实现线程本地存储的功能。

每天加个彩蛋

火车站碰到一个女孩,自称是大学生,钱包被扒,要我行善,并掏出学生证要我看。看着她真诚的双眼,着实想掏钱,突然看到学生证上赫然写着软件工程,灵光闪现问她:“冒泡排序的复杂度是多少?”她一下愣住了。一看不对,换个难度低点的:“C语言是面向对象还是面向过程?”她竟然落荒而逃!

成功的基础源于坚持,扫描指纹







































在北京治疗白癜风要多少钱
北京治疗白癜风最好的特效药

转载请注明原文网址:http://www.gzdatangtv.com/cksc/7150.html
------分隔线----------------------------