Java内存模型
Java的内存模型和物理机所对应的内存模型很相似,在Java内存模型中,线程都有单独的工作内存,与处理器的高速缓存有异曲同工之妙,多个线程直接与自己的工作内存进行交互,工作内存通过Load/Save与主内存进行交互,而不是直接与主存进行操作。
volatile的理解
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile之后,它将具备两项特性:
-
对其他线程的可见性:当一个线程修改了这个volatile修饰的变量,对于其他线程来说是立即得知的,而普通变量不能保证对其他线程的可见性。
-
禁止指令重排序优化:程序代码在执行时,执行顺序不一定非得要按照编写的代码顺序来执行,比如
int a=1; int b=1;
这两句代码在编写时定义变量a是在定义变量b之前的,但是在执行过程中,b可能在a之前进行定义,这是因为a和b没有必然的逻辑前后顺序,编译器在进行编译时可能会对其进行优化处理。但使用volitate关键字将禁止指令重排序
❗❗❗ violate只能保证简单变量在读写操作的原子性,不能保证自增自减的原子性
violate关键字使用场景:
✅运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
✅变量不需要与其他状态变量共同参与不变约束
线程安全
-
线程安全的定义:当多个线程同时访问同一个对象时,如果不用考虑这些对象在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的安全协作,调用这个对象的行为都可以获得正确的结果,则称这个对象线程安全的
-
线程安全的级别(由强到弱)
- 不可变:不可变的对象一定是线程安全的,无论是对象方法的实现还是方法的调用者,都不需要进行额外的线程安全保障操作。如Java中的基本数据类型如果在定义时被声明为final类型,就是不可变的
- 绝对线程安全:
- 相对线程安全:在Java语言中,大部分声称线程安全的类都属于这种类型。
- 线程兼容:线程兼容是指对象本身并不是线程安全的,需要在调用端进行线程安全保证手段来保证线程安全。Java类库中的大部分API(ArrayList、HashMap)都是线程兼容的
- 线程对立:无论调用端是否采取了同步措施,都不是线程安全的。
-
线程安全的实现方法
-
互斥同步
互斥同步是实现线程安全的常见手段,同步是指当多个线程访问共享数据时,保证共享数据在同一时刻只能被一条(或者是一些,当使用信号量的时候)线程使用。互斥是一种同步的手段,临界区、互斥量、信号量都是常见的互斥实现方式。在Java中,主要有两种互斥同步手段:
-
synchronized关键字:这是一种块结构的同步语法,在经过编译过后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码需要一个reference类型的参数来指定锁定和解锁对象。如果没有指定对象,则根据方法类型来决定。被synchronized修饰的同步块对同一线程来说是可重入的,但synchronized是Java语言中的一个重量级操作,因为它会阻塞后面的线程,引起操作系统用户态和核心态的转换
-
Lock接口:自JDK1.5起,Java类库中提供了基于Lock接口的一种全新的互斥同步手段。其中重入锁(ReentrantLock)和重入读写锁(ReentrantReadWriteLock)是其常见的实现。
两种互斥手段的比较:
Lock和synchronized功能类似,但Lock有一些高级功能,主要有等待可中断、公平锁、锁绑定多个条件。
性能上在jdk1.6之后,对synchronized进行了优化,导致其性能和Lock不相上下。
-
-
非阻塞同步
互斥同步一般来说是属于阻塞同步(其余要使用共享区的线程阻塞等待直到当前线程使用完毕)的,是一种悲观的并发策略。而非阻塞同步则是一种基于冲突检测的乐观并发策略:先进行操作,如果没有出现线程安全问题,就操作成功;如果出现线程安全问题,再进行其他的补偿措施,最常见的补偿措施就是不断重试,直到操作成功为止。非阻塞同步非常依赖硬件指令集,因为要保证操作和冲突检测的原子性
CAS:CAS全程Compare-and-swap,即比较并交换,是一条硬件指令。CAS会将变量的值与旧值进行比较,如果相同,则用新的值替换变量的值;如果不同,则不进行更新操作。通过循环CAS即可实现原子操作,但也存在一些问题:
- ABA问题:通过添加版本号解决
-
循环时间过长,cpu利用率降低:通过自适应自旋方式解决
3. 只能保证一个共享变量的原子操作 -
无同步方案
有一些代码天生就是线程安全的,因此不需要同步。主要有两类:
- 可重入代码
- 线程本地存储
-
锁优化
-
自旋锁和自适应自旋
互斥同步对性能最大的影响是阻塞的实现,挂起和恢复线程都需要操作系统内核态的支持。但一般而言某一个线程对共享变量的占用时间是极短的,因此我们可以让线程执行忙循环(自旋)。当自旋的次数或时间到了,如果还没得到资源,便进入阻塞状态。自适应自旋就是根据上一次自旋时间以及锁的状态来决定自旋的时间或次数,而不是使用统一的自旋时间或次数
-
锁消除
编译器再进行编译时会进行优化,对于某些加了锁但实际没必要的锁会进行忽略加锁
-
锁粗化
我们一般要求同步块的作用范围应该设置的尽量小,从而使需要同步的操作数量变小。但如果对同一对象反复加锁和解锁是没必要的。如果虚拟机探测到一串零碎的操作对同一个对象加锁,就会将加锁的范围变大。
-
轻量级锁
轻量级锁是相对于重量级锁而言,但不是用来替代重量级锁的。它能提升性能的依据是:
对于绝大部分锁,在整个同步周期内都是不存在竞争的。
当线程没有出现竞争时,就避免了互斥量的开销,但是如果有竞争,除了互斥量本身的开销,还要加上轻量级锁的CAS操作的开销,反而影响了性能。
-
偏向锁
偏向锁的目的是消除在无竞争情况下的同步原语,进一步提高程序的性能。偏向锁也即这个锁会偏向于第一个获得它的线程,如果在接下来的执行中,该锁一直没被其他线程获取,则偏向锁的线程将不再进行同步操作。