十二、十三章 java内存模型、线程安全

原子性 可见行 有序性

原子性

由Java内存模型来直接保证的原子变量由:read load assign use store write 这些基本数据类型的访问读写

synchronized块之间的操作也具备原子性

可见行(Visibility)

是指当一个线程修改了共享变量的值,其他线程可以立即得知这个修改。Java内存模型是怎么实现可见行呢?利用依赖主内存,一个变量更改了就要同步会主内存,在读取这个变量的时候要从主内存刷新变量值来实现。普通变量无法保证这一点,violate变量可以保证。

synchronized final关键字同样可以。

synchronized在unlock之前要保证变量回主内存。这样就可以用主内存刷出最新变量值
final 关键字通过:被final修饰的字段在构造器中完成初始化,并且构造器没有把this的引用传递出去, 那在其他线程中就能看见final字段的值了。

有序性(Ording)

利用volatile和synchronized关键字完成线程之间的有序性。volatile本身就有禁止指令重排的语义,而synchronized关键值通过“一个变量在同一时刻只允许一条线程对其进行lock操作。“

先行发生原则

下面是一些天然的先行发生关系:

程序次序顺序:在同一个线程中,程序流循序。

管程锁定规则:一个unlock操作先行发生于后面对于同一个锁的lock操作。这里,我们指的是同一个锁,并且在时间上的先后。

volatile;

。。。。。

并发问题的时候我们要以先行发生的原则为准,而不以时间顺序的干扰。

Java与线程

实现线程的3种方式:

内核线程实现

KLT内核线程

用户线程实现

进程与用户线程之间1:N的关系。用户线程有优缺点:优点就是不需要内核的支援,缺点也是这样。所有的线程操作都要用户程序自己处理。线程的创建和调度都需要自己考虑。

用户线程+轻量级进程混合实现

Java的线程调度

抢占式

由系统分配执行时间,线程的切换不由线程本身来决定。Java就是抢占式。

协同式

线程的执行时间由线程本身来控制。好处是实现简单,没有什么线程同步问题,切换操作对于线程来说是可见的。缺点是,如果一个线程有问题了,会一直不告知系统线程切换,就会让系统直接崩了。

状态转换
十二、十三章 java内存模型、线程安全_第1张图片

新建:创建后尚未启动的线程处于的状态;

运行:Runnable,包含了Running-正在执行的状态以及Ready-等待CPU为它分配执行时间。

无限期等待:处于这种状态的线程不会分配CPU执行时间。需要被 唤醒。

限期等待:不需要等待唤醒,会自动唤醒。

阻塞:一直在等一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生。是发生同步区域时候。

结束:已终止线程。

第十三章-线程安全

    Java中,各种操作共享的数据分为以下5类。

1.不可变

不可变的对象一定是线程安全的。被final修饰的基本数据类型是不可变的。对象也可以是不可变的,前提是对象的行为不会对其状态产生任何影响(只要将对象的所有字段都用final修饰即可)String、Integer、Long、Double等都是不可变对象。

2.绝对线程安全

一个类要想达到绝对线程安全,需要做到无论什么时候,都不需要额外的同步措施。这需要很大的代价。

3.相对线程安全

这就是我们通常意义上的线程安全,它只需要保证对对象单独的一个操作是线程安全的。

4.线程兼容

线程兼容是指对象本身并不是线程安全的,单可以通过在调用时正确使用同步手段来保证对象在并发环境中可以安全使用,我们平时说一个类不是线程安全的,通常是指这种情况。

5.线程对立

线程对立指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。如Thread类的suspend和resume方法。

线程安全的实现方法

互斥同步
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是几个,在使用信号量时)线程使用,而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。互斥是因,同步是果;互斥是方法,同步是目的。
Java中最基本的互斥同步手段就是synchronized关键字,它对同一个线程来说是可重入,当一个线程在同步块中执行的时候,会阻塞后面其他线程的进入。另外还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步,相比synchronized,ReentrantLock增加了一些高级功能:等待可中断、可实现公平锁以及锁可以绑定多个条件:

等待可中断是指线程在尝试获取锁时可以指定一个等待时间,若锁被其他线程持有,则休眠等待,如果经过等待时间仍未获取到锁,则放弃等待。

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,不过可以设置为公平的。

锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象。

非阻塞同步
互斥同步是一种悲观的并发策略,即认为不做同步就会出错,访问共享数据时不论是否真的存在竞争都会加锁,最主要的问题是进行线程阻塞和唤醒时都要陷入内核,开销较大。
非阻塞同步是一种基于冲突检测的乐观并发策略,就是认为多个线程进行争用是很少发生的,因此可以先进行操作,如果没有和其他线程发生争用就成功了,如果有争用产生了冲突,那就再采取其他的补偿措施(最常见的就是不断重试直至成功)。

实现非阻塞同步需要硬件支持,通常就是通过CAS(Compare And Swap)原语来实现的。

CAS操作:
compare-and-swap就是当且仅当V符合旧的预期值A时候,我们才将新值B更新V值。否则,我们就不执行将B更新V的操作。无论是否更新,都会返回V的旧值,上述操作是一个原子操作。

无同步方案

可重入代码,可重入代码一定是线程安全的,就不需要同步了。

线程本地存储,就是说将共享数据的可见部分控制在一个线程中。例如,一个请求对应一个服务器线程。可以通过threadLocal来实现本地存储功能。

锁优化

https://www.jianshu.com/p/73b9a8466b9c https://www.jianshu.com/p/73b9a8466b9c https://www.jianshu.com/p/73b9a8466b9c

自旋锁与自适应自旋
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。如果同步块内的操作很少,线程可以很快地完成同步块内的操作,那么很可能挂起线程和恢复线程所花费的时间比执行同步块内代码的时间还要长。这种情况下,如果计算机是多核系统,即多个线程可以分别在多个cpu上同时执行,此时一个线程在获取锁时如果发现锁已被其他线程持有,与其休眠等待,不如忙等待一段时间,因为持有锁的线程可能很快就会释放锁。这就是自旋锁。

如果锁被占用的时间很短,自旋等待的效果非常好,因为省去了大量的挂起线程和恢复线程的动作。但如果锁被占用的时间很长,那么自旋等待则会带来额外的性能浪费,因为自旋之后还是要休眠等待。

在JDK1.6之前,自旋次数是固定的,默认值是10次,之后自旋次数则不是固定的了,而是自适应的了,是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果自旋等待总是可以成功,那么虚拟机会认为下一次还可以成功,从而增加自旋等待的次数;相反,如果自旋等待总是失败,则虚拟机则会将自旋等待省略掉,以免额外的性能浪费。

锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据来源于逃逸分析的数据支持,如果即时编译器判断一段代码中的同步块所保护的对象压根就不会被多个线程同时访问,则可以将相应的加锁和解锁操作删除,从而消除不必要的同步操作,提高程序的性能。

锁粗化
原则上总是推荐将同步块的作用范围限制得尽量小,同步块内的语句尽可能少,这可以尽快的释放锁,其他等待锁的线程也能尽快拿到锁。但是如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至是在循环体中不停的加锁解锁操作的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,此时虚拟机就会把将锁同步的范围扩大(粗化)到整个操作序列的外部,以减少加锁和解锁操作的次数,从而提高性能。

总结如下:

轻量级锁:多个线程交替进入临界区;

偏向锁:只有一个线程进入临界区;

重量级锁:多个线程同时进入临界区。

先说锁的状态:

锁的状态共有四种:无锁,偏向锁,轻量锁,重量锁。随着锁的竞争,锁会从偏向锁升级为轻量锁,然后升级为重量锁。锁的升级是单向的,JDK1.6中默认开启偏向锁和轻量锁。

偏向锁
引入偏向锁的目的是:为了在无多线程竞争的情况下,尽量减少不必要的轻量锁执行路径。因为经过研究发现,在大部分情况下,锁并不存在多线程竞争,而且总是由一个线程多次获得锁。因此为了减少同一线程获取锁(会涉及到一些耗时的CAS操作)的代价而引入。 如果一个线程获取到了锁,那么该锁就进入偏向锁模式,当这个线程再次请求锁时无需做任何同步操作,直接获取到锁。这样就省去了大量有关锁申请的操作,提升了程序性能。

轻量级锁

轻量锁能够提升性能的依据,是基于如下假设:即在真实情况下,程序中的大部分代码一般都处于一种无锁竞争的状态(即单线程环境),而在无锁竞争下完全可以避免调用操作系统层面的操作来实现重量锁。如果打破这个依据,除了互斥的开销外,还有额外的CAS操作,因此在有线程竞争的情况下,轻量锁比重量锁更慢。
为了减少传统重量锁造成的性能不必要的消耗,才引入了轻量锁。

获取轻量锁:

判断当前对象是否处于无锁状态(偏向锁标记=0,无锁状态=01),如果是,则JVM会首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储当前对象的Mark Word拷贝。(官方称为Displaced Mark Word)。接下来执行第2步。如果对象处于有锁状态,则执行第3步

JVM利用CAS操作,尝试将对象的Mark Word更新为指向Lock Record的指针。如果成功,则表示竞争到锁。将锁标志位变为00(表示此对象处于轻量级锁的状态),执行同步代码块。如果CAS操作失败,则执行第3步。

判断当前对象的Mark Word 是否指向当前线程的栈帧,如果是,则表示当前线程已经持有当前对象的锁,直接执行同步代码块。否则,说明该锁对象已经被其他对象抢占,此后为了不让线程阻塞,还会进入一个自旋锁的状态,如在一定的自旋周期内尝试重新获取锁,如果自旋失败,则轻量锁需要膨胀为重量锁(重点),锁标志位变为10,后面等待的线程将会进入阻塞状态。

释放轻量锁:
轻量级锁的释放操作,也是通过CAS操作来执行的,步骤如下:

取出在获取轻量级锁时,存储在栈帧中的 Displaced Mard Word 数据。

用CAS操作,将取出的数据替换到对象的Mark Word中,如果成功,则说明释放锁成功,如果失败,则执行第3步。

如果CAS操作失败,说明有其他线程在尝试获取该锁,则要在释放锁的同时唤醒被挂起的线程。

重量级锁

重量级锁通过对象内部的监视器(Monitor)来实现,而其中monitor本质上是依赖于低层操作系统的 Mutex Lock实现。操作系统实现线程切换,需要从用户态切换到内核态,切换成本非常高。

你可能感兴趣的:(深入理解Java虚拟机)