明天你会感谢今天奋力拼搏的你。
ヾ(o◕∀◕)ノヾ
原子操作:原则操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。
竟态条件:当两个线程竞争同一个资源时,如果对资源的访问顺序敏感,就称存在竟态条件。
临界区:导致静态条件发生的代码区。
CAS 属于硬件同步原语,处理器提供的内存操作指令,保证原子性。
CAS 操作需要两个参数,一个旧值和一个目标值,修改前先比较旧值是否改变,如果没变,将新值赋给变量,否则不做改变。
JAVA中的sun.misc.Unsafe类提供了CAS机制(内存中同一时刻只能一个线程对其修改)。
基本Java对线程安全操作,最后底层都调用了unsaffe如下三个方法之一:
有4个参数:
compareAndSwapObject要修改第一个参数的值为第四个参数,那么第三个参数就传null。
JDK9之后sun.misc.Unsafe类被jdk.internal.misc.Unsafe替代,可看另一篇文章
原子更新基本类型类:
原子更新数组:
原子更新引用类型:
原子更新字段类:
Java1.8新增计数器:
ABA问题详解:
ABA的问题什么情况会出现?
怎么避免ABA?
可以对i的修改加个版本。
Java的原子操作封装类中有版本号的原子操作AtomicStampedReference,除了比对值之外还得比对版本号。
乐观锁适合读多的场景,写场景性能偏低。
可重入锁同步代码块中锁了多次会没有效果,会继续执行到解锁,场景经常用在递归。
不可重入锁,第二次加锁会阻塞。
几种重要的锁实现方式:synchronized、ReentrantLock(可重人锁)、ReentrantReadWriteLock
使用方式:
锁的作用域:对象锁、类锁、分布式锁
特性:可重入、独享(互斥)、悲观锁
锁消除:如果代码中在单线程中使用了锁,如果进入了JIT编译,会自动优化,进行锁消除。
开启锁消除的参数:-XX:+DoEscapeAnalysis -XX:+EliminateLocks
锁粗化:JDK做了锁粗化的优化。比如:
synchronized放在for循环的第一行,在for内层进行加锁,那么每次循环就会去抢锁,耗费资源,进入JIT编译中会被自动优化到for循环外层。
多个synchronized代码块持有相同的锁,顺序排列到一起执行,那么可以粗化为一个。
Note: 有些锁粗化我们自己可直接从代码层面优化完成。synchronized关键字,不仅实现同步,JMM中规定,synchronized要保证可见性(不能够被缓存)。
synchronized使用很简单,但是这个关键字具体是怎么实现的呢?JVM怎么知道代码上面加了锁?
若锁占用,线程挂起,释放锁时,如何做到唤醒挂起的线程?
JVM在堆内存中为一个对象分配一个空间后,应该会用一种方式把这个对象和方法区中存储的类信息(元信息)关联起来,怎么关联呢?
就是在为对象分配的空间中,除了存储字段信息外,还有一个对象头,对象头中就存储了有方法区中元信息的引用,当然除了元信息引用外还有其他关键信息,如下图所示:
先解释一下,Mark Word是一块内存区域,里面存的值是上面表格红框部分的某一行。
具体存的哪一行根据右边的State决定。
Mark Word这块内存区域多大?根据JDK是32位还是64位,32位内存区域就32位大小,64就64位大小。
Bitfields(bit域)、tag(状态位)、state(状态)、Hashcode(当前对象的hashcode)、age(垃圾分代回收中的age)
默认情况下JVM锁会经历:未锁定->偏向锁-> 轻量级锁-> 重量级锁这四个状态
流程分析:
对象创建一开始先是未锁定。
当多个线程要抢这个对象的锁,会用cas操作把tag中的01改成00,让状态变成轻量级锁。
如果某个线程将状态位改成了00,然后就会将00前面的位数改成这个线程引用(Lock record address,这里的地址就是当前线程的地址)
其他没抢到的线程会进行自旋,自旋会有一定的次数阈值,达到一定数量会升级为重量锁。
怎么升级为重量级锁?
先要介绍一个东西Monitor(监视器),monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。
JVM把状态位由00改为10.
然后把Lock record address改成monitor address。
那么谁获得了锁记录在哪里?记录在Monitor中有个onwer字段,会标记谁获取了锁。
升级为重量级锁是为了解决其他线程不再自旋消耗资源的问题,那么具体怎么解决的?
Monitor中还有一个EntryList(锁池)字段,相当于一个等待队列。
JVM先会把等待线程的引用存入这个队列中。
然后JVM让等待线程挂起。线程就会处于Blocked状态。
然后比如持有锁的线程1,在代码块中调用了wait方法,那么会发生什么情况?
线程1会挂起线程。
onwer会释放掉这个线程的引用,onwer=null
然后这个线程的引用会被放入Monitor的另一个字段waitSet(等待池)中。
其他所有等待线程去抢锁。
假设线程2抢到了锁,如果线程2中执行了notify方法,那么会把waitSet中的一个等待线程拿出来,抢一次锁(onwer),没抢到放入Entrylist中。(notifyAll也一样,只是释放waitSet里所有线程)
然后线程2的代码块执行完后,执行解锁操作,从monitor中退出(exit monitor)。
那么onwer又变成了null。
轻量级锁和重量级锁都是多线程的情况下。
还有一种情况,虽然加了synchronized关键字,但是程序是在单线程中执行。
在JDK6 以后,默认已经开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking 来禁用偏向锁若偏向锁开启。
如上图就是偏向锁的Mark Word存储的字段。
如果只有一个线程抢锁,可获取到偏向锁。
如果生成偏向锁后又有线程来抢锁,那么会把锁升级为轻量级锁。
一旦锁升级,偏向锁就会关闭,不会回退为偏向锁。
如果重量级锁中的线程都释放了,那么会退回到未锁定的状态。
偏向标记第一次有用,出现过争用后就没用了。 -XX:-UseBiasedLocking 禁用使用偏置锁定,
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争抢,就不需要去做同步操作)
三、Lock锁
全部评论