文章目录
CAS(compare and set)
- 概述
CAS是由硬件实现的,可以将read-modify-write这类的操作转化为原子操作,原子变量类就是基于CAS实现的,AQS中也大量使用了CAS
- 原理
在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(即操作起始时读取的值)一样就更新。共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过
CAS实现原子操作的三大问题:
- ABA问题:
如果一个共享变量的值被修改了两次又修改回原来的值了,那么我们是否要认为这个变量被修改过?这种类型的问题我们成为ABA问题
(A-->B-->A)
解决
:对于这种问题的解决方式就司空见惯了,不论就是加个表或标记来记录被修改的次数,所以如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1,由此诞生出一个类为AtomicStampedReference
- 循环时间长开销大:
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销;
解决
:如果JVM支持处理器中的pause指令(可自行查阅资料,大致有两个作用:①.延迟流水线执行指令;②.避免在退出循环的时候 因内存顺序冲突而引起CPU流水线被清空),效率就会有提高
- 只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性
解决
:这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来 操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始, JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对 象里来进行CAS操作。
synchronized
1.synchronized关键字的底层原理
1.1 synchronized修饰对象
- 前言:
一直以来synchronized被称为重量级锁,但自jdk1.6之后引入了偏向锁和轻量级锁从而减轻获得锁和释放锁带来的性能消耗,Java中的每一个对象都可以作为锁,具体表现为三种形式。
synchronized关键字的修饰对象
所以对于syn的静态方法和实例所属方法的区别就是锁对象的不同
- 对于普通同步方法,锁就是当前实例对象
- 对于静态同步方法,锁就是当前类的class对象
- 对于同步方法块,锁就是synchronized括号里的配置对象
synchronized关键字修饰对象示例:
关键字位置在哪,就是在哪里开始生效,从而衍生出来同步的开始结束范围
- synchronized关键字修饰代码块
- 修饰实例方法就称为同步实例方法;修饰静态方法称称为同步静态方法
- 修饰代码块的语法:
synchronized(对象锁){
//想要同步必须是同一个锁对象,即传进去的锁对象是一致的
//同步代码块,可以在同步代码块中访问共享数据
}
- 修饰方法的语法:
默认的锁对象是this对象
public synchronized void XXX(){ }
- 修饰静态方法的语法:
说明:可以直接传入当前类的字节码文件作为对象,而静态同步方法的默认锁对象就是本类的字节码文件对象
public class Test01 {
public void mm(){
synchronized (Test01.class){}
}
public synchronized static void ssm(){ }
}
1.2 内存语义
- 对于同步块
将代码编译后,会有两个对应的指令
monitorenter
和monitorexit
,前者放置在同步代码块的开始位置,后者则放置在方法结束处和异常处,JVM会将每个monitorenter都有一个monitorexit对应,两个指令是属于monitor对象,任何一个对象都一个monitor对象与之关联,当monitor被持有后,将处于锁定状态,线程执行指令的时候将尝试获取对象对应的monitor的所有权,即尝试获取对象的锁,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
- 对于同步方法
方法同步是使用另外一种方式实现的,细节在JVM规范里并没有 详细说明。但是,方法的同步同样可以使用这两个指令来实现。
Java的对象头:
synchronized关键字用的锁是存放在java的对象头中的
-
对象头在Java内存中宏观存储位置如下图
-
Java对象头的长度
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
- Mark Word的存储结构(32位JVM)
默认存储对象的HashCode,分代年龄和锁标志位信息
状态变化:
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据
2.多线程竞争锁时的锁状态迁移(锁升级)
2.1 概述
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:
无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态
,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率
2.1 无锁
- 概述
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。典型代表就是
CAS
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
2.3 偏向锁
- 概述
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
偏向锁设置过程:
- 线程成功获取锁,会在Mark Word和栈帧里存放锁偏向的线程ID;后续加锁解锁都不需要进行CAS操作
- 下一次该线程获取该锁时,会简单的测试一下Mark Word中线程ID是否为当前ID,成功获取
- 测试不成功,则要看锁对象的Mark Word的偏向锁标志位是否为1(表示当前是偏向锁)
- 锁对象没有设置则CAS竞争锁
- 如果当前锁对象设置成偏向锁了,就去CAS看能否将Mark Word设置为当前线程的ID
偏向锁的撤销(升级为轻量级锁):
- 触发时机
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待
全局安全点
(在这个时间点上没有正 在执行的字节码)。
- 暂停拥有偏向锁的线程,看持有线程是否为运行态,不是则将对象头设置为无锁状态
- 如果为运行态,此时会遍历栈帧查看锁对象的所记录,根据记录会有以下三种情况发生:①.该记录和对象头的Mark Word会偏向于其他线程;②.恢复到无锁
(标志位为“01”)
;③.标记该锁对象不适合作为偏向锁,发生锁升级(标志位为“00”)
- 最后唤醒暂停的线程
偏向锁的关闭:
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如 有必要可以使用JVM参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0
。如果你确定应用程 序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:- UseBiasedLocking=false
,那么程序默认会进入轻量级锁状态。
2.4 轻量级锁
- 概述
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
轻量级锁加锁:
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”)
- JVM会先在当前线程的栈桢中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
- 然后线程尝试使用 CAS将对象头中的Mark Word更新为指向
锁记录
的指针。并将锁记录
里的owner指针指向对象的Mark Word。- 成功则当前线程获取锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
- 失败,JVM会去检查锁对象的Mark Word是否指向当前线程的栈帧,指向则说明已经获取锁了
- 没有就说明有多个线程在进行锁竞争
轻量级锁解锁(升级为重量级锁):
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
- 轻量级解锁时,会使用原子的CAS操作将对象头中的Mark Word替换为原先的内容,如果成功,则表示没有竞争发生。
- 如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
不存在锁降级:
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时, 都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮 的夺锁之争。
2.5 重量级锁及锁的优缺点对比
- 概述
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
- 锁升级的整个流程
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
锁的优缺点对比:
volatile
1.volatile关键字的作用和底层原理
1.1 概述及底层原理
- 概述:
volatile是轻量级的synchronized,在处理多线程时能保证可见性,即一个线程修改一个共享变量时,另外所有线程能读到这个修改的值,不会引起线程调度和上下文切换
- 底层原理
在加上volatile关键字的变量底层对应的汇编语句中会加上lock的字样,此语句会引发两个变化,这两个变化后为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当发现过期了会讲此缓存行设置为无效状态,当进行修改时进行同步从系统内存读取
①.将当前处理器缓存行的数据写回到系统内存
:在写回过程中,会将这块的内存区域锁定,并使用缓存一致性原则来保证修改的原子性,此操作为缓存锁定
②.这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效
:那么对于其他CPU的缓存行无效,在下次访问相同内存地址时,会强制执行缓存行填充
volatile的优化:
在jdk7的并发包中,新增了一个队列集合类LinkedTransferQueue,使用volatile变量时,用一种追加字节(64字节,一个对象引用4个字节,追加15个变量共占60字节)的方式来优化出队和入队的性能,追加成64字节能提高效率是因为高新能处理器的高速缓存都是64个字节宽,当队列的头节点和尾节点都不足会读到同一个缓存行中,如果使用64字节来填充那么可以避免头和尾的相互锁定。
什么情况下可以不用volatile优化
:缓存行非64字节宽的处理器、共享变量不会被频繁地写。在越来越来智慧的JDK来说这种方式的优化使用的越来越少
1.2 volatile的内部语义
- 概述
根据测试使用volatile变量的单个读或写,与使用普通变量的读写用锁来同步执行效果相同,用happens-before来保证可见性,锁的语义保证原子性。如果是多个volatile操作或volatile++的符合操作整体上是不具备原子性的
- 特性
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不 具有原子性。
1.3 volatile写、读(读与写肯定有h-b的约束)
- 写volatile变量时:JMM会把线程对应的本地内存的共享变量刷新到主内存中
- 读volatile变量时:JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量,即该变量对读的线程可见了
总结:
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。
1.4 volatile的内存语义实现
- 先来了解下volatile重排序规则表
从表中可看出,当进行读操作的时候尤为严格,要实现这个效果在底层执行对于volatile之前之后都会有相应的指令,来实现内存屏障
- 内存屏障策略(当然针对不同的情况可以有不同的策略改变,即代表的意思会改变)
- 示意图
1.5 总结
在jdk5以前旧的模型中,不允许volatile变量之间重排序,但允许volatile变量与普通变量之间重排序,为了实现一款轻量级的锁在jdk5以后的内存模型对volatile的限制更加明确即增强了volatile的语义
- 缺点
由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以 确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行 性能上,volatile更有优势。如果读者想在程序中用volatile代替锁,请一定谨慎