文章目录
原子性、有序性、可见性
原子性
数据库事务的原子性:是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。
并发编程的原子性:一个或多个指令在CPU执行过程中不允许中断。
i++;操作是原子性?
肯定不是:i++操作一共有三个指令
getfield:从主内存拉取数据到CPU寄存器
iadd:在寄存器内部对数据进行+1
putfield:将CPU寄存器中的结果更新搭配主内存中
如何保证i++是原子性?
使用synchronized、lock、Atomic(CAS)来保证
使用lock锁也会有类似的概念,也就是在操作i++的三个指令前,先基于AQS成功修改state后才可以操作
使用synchronized和lock锁时,可能会触发将线程挂起的操作,而这种操作会触发内核态和用户态的切换,从而导致消耗资源。
CAS方式就相对synchronized和lock锁的效率更高,因为CAS不会触发线程挂起操作!
CAS:compare and swap
线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改
CAS就是比较和交换,而比较和交换是一个原子操作
CAS在Java层面就是Unsafe类中提供的一个native方法,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略需要自己实现
CAS问题:
- CAS只能对一个变量的修改实现原子性。
- CAS存在ABA问题。
- A线程修改主内存数据从1~2,卡在了获取1之后。
- B线程修改主内存数据从1~2,完成。
- C线程修改主内存数据从2~1,完成。
- A线程执行CAS操作,发现主内存是1,没问题,直接修改
- 解决方案:加版本号
- 在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
- synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
- LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加
有序性
指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。但是这样可能会造成数据的不一致。
如果不希望CPU对指定进行重排序,怎么办?
可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。
可见性
CPU在处理时,需要将主内存数据拿到寄存机中再执行指令,执行完指令后,需要将寄存器数据扔回到主内存中。但是寄存器数据同步到主内存是遵循MESI协议的,简单来说就是:不是每次操作结束就将CPU缓存数据同步到主内存,这样就会造成多个线程看到的数据不一样。
所以通常需要synchronized和volatile配合解决这一问题:
- volatile每次操作后,立即同步数据到主内存。
- synchronized,只有一个线程操作这个数据。
synchronized使用
使用方法:声明方法时使用synchronized或者在代码块中使用synchronized。
锁类型:
- 类锁:基于当前类的Class加锁
- 对象锁:基于this对象加锁
synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!
synchronized是如何基于对象实现的互斥锁,先了解对象再内存中是如何存储的。
在Java中查看对象的存储:
导入依赖:
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
查看对象信息
synchronized锁升级
synchronized在jdk1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程。
jdk1.6之前synchronized效率贼低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在jdk1.6将synchronized做优化。
锁升级:
- 无锁状态、匿名偏向状态:没有线程拿锁。
- 偏向锁状态 :没有线程的竞争,只有一个线程在获取锁资源。
线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。 - 轻量级锁 :偏向锁出现竞争时,会升级到轻量级锁。
轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。 - 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。重量级锁就是线程拿不到锁,就挂起。
偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向synchronized因为不存在从重量级锁降级到偏向或者是轻量。
synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,stw,才可以撤销,并发偏向锁撤销比较消耗资源。在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作。为了避免启动时涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。
编译器优化的结果,出现了下列效果
-
锁消除:线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。
-
锁粗化:在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。
synchronized-ObjectMonitor
ObjectMonitor一般是到达了重量级锁才会涉及到。在到达重量级锁之后,重量级锁的指针会指向ObjectMonitor对象。
hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 抢占锁资源的线程个数
_waiters = 0, // 调用wait的线程个数。
_recursions = 0; // 可重入锁标记,
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // wait的线程 (双向链表)
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ; // 假定的继承人(锁释放后,被唤醒的线程,有可能拿到锁资源)
_cxq = NULL ; // 挂起线程存放的位置。(单向链表)
FreeNext = NULL ;
_EntryList = NULL ; // _cxq会在一定的机制下,将_cxq里的等待线程扔到当前_EntryList里。 (双向链表)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}