原子性
定义:对于涉及到共享变量访问的操作,若该操作从执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性。即,其它线程不会“看到”该操作执行了部分的中间结果。
举例:银行转账流程中,A账户减少了100元,那么B账户就会多100元,这两个动作是一个原子操作。我们不会看到A减少了100元,但是B余额保持不变的中间结果。
原子性的实现方式
- 利用锁的排他性,保证同一时刻只有一个线程在操作一个共享变量
- 利用CAS(Compare And Swap)保证
- Java语言规范中,保证了除long和double型以外的任何变量的写操作都是原子操作
- Java语言规范中又规定,volatile关键字修饰的变量可以保证其写操作的原子性
注意
- 原子性针对的是多个线程的共享变量,所以对于局部变量来说不存在共享问题,也就无所谓是否是原子操作
- 单线程环境下讨论是否是原子操作没有意义
- volatile关键字仅仅能保证变量写操作的原子性(它一修改,其它线程都能看见),不保证复合操作,比如说读写操作的原子性(读写操作比如i++)
可见性
定义:可见性是指一个线程对于共享变量的更新,对于后续访问该变量的线程是否可见的问题。
为了阐述可见性问题,我们先来简单介绍处理器缓存的概念。
现代处理器处理速度远大于主内存的处理速度,所以在主内存和处理器之间加入了寄存器,高速缓存,写缓冲器以及无效化队列等部件来加速内存的读写操作。也就是说,我们的处理器可以和这些部件进行读写操作的交互,这些部件可以称为处理器缓存。
处理器对内存的读写操作,其实仅仅是与处理器缓存进行了交互。一个处理器的缓存上的内容无法被另外一个处理器读取,所以另外一个处理器必须通过缓存一致性协议来读取的其他处理器缓存中的数据,并且同步到自己的处理器缓存中,这样保证了其余处理器对该变量的更新对于另外处理器是可见的。
在单处理器中,为什么也会出现可见性的问题呢?
单处理器中,由于是多线程并发编程,所以会存在线程的上下文切换,线程会将对变量的更新当作上下文存储起来,导致其余线程无法看到该变量的更新。所以单处理器下的多线程并发编程也会出现可见性问题的。
可见性如何保证?
- 当前处理器需要刷新处理器缓存,使得其余处理器对变量所做的更新可以同步到当前的处理器缓存中
- 当前处理器对共享变量更新之后,需要冲刷处理器缓存,使得该更新可以被写入处理器缓存中
有序性
定义:有序性是指一个处理器上运行的线程所执行的内存访问操作在另外一个处理器上运行的线程来看是否有序的问题。
重排序:
为了提高程序执行的性能,Java编译器在其认为不影响程序正确性的前提下,可能会对源代码顺序进行一定的调整,导致程序运行顺序与源代码顺序不一致。
重排序是对内存读写操作的一种优化,在单线程环境下不会导致程序的正确性问题,但是多线程环境下可能会影响程序的正确性。
重排序案例
Instance instance = new Instance()都发生了啥?
具体步骤如下所示三步:
- 在堆内存上分配对象的内存空间
- 在堆内存上初始化对象
- 设置instance指向刚分配的内存地址
第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。(在多线程下的单例模式中,我们必须通过volatile来禁止指令重排序)
总结三大性
- 原子性是一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。注意,原子操作+原子操作不一定还是原子操作。
- 可见性是指一个线程对共享变量的更新对于另外一个线程是否可见的问题。
- 有序性是指一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题。
可以这么认为,原子性 + 可见性 -> 有序性
synchronized
synchronized是Java中的一个关键字,是一个内部锁。它可以使用在方法上和方法块上,表示同步方法和同步代码块。在多线程环境下,同步方法或者同步代码块在同一时刻只允许有一个线程在执行,其余线程都在等待获取锁,也就是实现了整体并发中的局部串行。
内部锁底层实现:
- 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
- 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态
synchronized内部锁对原子性的保证:
锁通过互斥来保障原子性,互斥是指一个锁一次只能被一个线程所持有,所以,临界区代码只能被一个线程执行,即保障了原子性。
synchronized内部锁对可见性的保证:
synchronized内部锁通过写线程冲刷处理器缓存和读线程刷新处理器缓存保证可见性。
- 获得锁之后,需要刷新处理器缓存,使得前面写线程所做的更新可以同步到本线程。
- 释放锁需要冲刷处理器缓存,使得当前线程对共享数据的改变可以被推送到下一个线程处理器的高速缓冲中。
synchronized内部锁对有序性的保证:
由于原子性和可见性的保证,使得写线程在临界区中所执行的一系列操作在读线程所执行的临界区看起来像是完全按照源代码顺序执行的,即保证了有序性。
公平调度方式
按照申请的先后顺序授予资源的独占权。
非公平调度方式
在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。
如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。
两者优缺点
非公平调度策略:
- 优点:吞吐率较高,单位时间内可以为更多的申请者调配资源
- 缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象
公平调度策略:
- 优点:线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象;适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;
- 缺点:吞吐率较小
JVM对synchronized内部锁的调度:
JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.
volatile
volatile关键字是一个轻量级的锁,可以保证可见性和有序性(它会禁止指令重排序),但是不保证原子性。
- volatile 可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
- volatile 仅能保证变量写操作的原子性,不能保证读写操作的原子性。
- volatile可以禁止指令重排序(通过插入内存屏障),典型案例是在单例模式中使用。
volatile变量的开销:
volatile不会导致线程上下文切换,但是其读取变量的成本较高,因为其每次都需要从高速缓存或者主内存中读取,无法直接从寄存器中读取变量。
volatile在什么情况下可以替代锁?
volatile是一个轻量级的锁,适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量。可以将多个线程共享的一组状态变量合并成一个对象,用一个volatile变量来引用该对象,从而替代锁。
ReentrantLock和synchronized
ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。
synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁。