目录
1、原子性问题
2、锁
2.1、实例锁
2.2、类锁
2.3、代码块
3、锁的存储
4、锁的类型
4.1、乐观锁
4.2、悲观锁
5、同步锁状态转换
5.1、无锁
5.1.1、基础信息
5.1.2、流程分析
5.1.3、升级总结
5.2、偏向锁
5.2.1、基础信息
5.2.2、流程分析
5.2.3、升级总结
5.2.4、批量重定向
5.3、轻量级锁
5.3.1、基础信息
5.3.2、流程分析
5.3.3、升级总结
5.3.4、重入计数
5.4、重量级锁
5.4.1、基础信息
5.4.2、流程分析
5.4.3、升级总结
6、synchronized的降级
7、synchronized的优化
一千个线程循环计数,最终结果总是小于等于1000的随机数。.
public class Count {
public static int count = 0;
public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Count.incr()).start();
}
Thread.sleep(2000);
System.out.println("结果:" + count);
}
}
在java中,加锁需要使用synchronized关键字,锁的本质就是对于共享资源访问的一个限制,它让同一时间内只有一个线程能访问这个共享资源,以此确保多线程并发的原子性操作,因此对于synchronized而言,加锁是有作用范围的,范围就是共享资源的使用范围。
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁,只针对于当前对象实例有效。
public class SynchronizedDemo {
synchronized void method1() {
}
void method2() {
synchronized (this) {
}
}
}
静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,针对所有对象都互斥,因为静态方法是唯一的,所以在静态方法上加锁也是类锁。
public class SynchronizedDemo {
synchronized static void method3() {
}
void method4() {
synchronized (SynchronizedDemo.class) {
}
}
}
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁,如单例模式给HashMap加锁。
public class SynchronizedDemo {
Object object = new Object();
void method5() {
synchronized (object) {
}
}
}
synchronize关键字是对某一个对象进行加锁,那么肯定会在某个位置上具有标记,线程可以根据标记判断是否加锁,那么可以确定的就是,标记应该在加锁对象上。
在java中对某一个对象加锁之后,在这个对象的JVM层面的存储结构中,对象头保存有关于锁的信息。
下表是32位存储的内容,64位与32位的存储是几乎没有差别的,可以看到在表中,除了无锁状态之外,还有三种锁,那么一个synchronized具有三种锁类型吗?
当两个线程抢占资源的时候,会经历三种类型的锁:偏向锁,轻量级锁,重量级锁,它们是同一把锁的三种状态。
一定要意识到,锁本身就意味着额外的性能消耗,因此为了提升性能,最好的办法是无锁,因此java对synchronized做出了一些优化,三种锁类型就是优化结果。
但是在了解synchronized之前,需要了解一下乐观锁与悲观锁
乐观锁的预期是乐观的,它默认不会有人修改数据,但是它又无法避免真的可能会有人修改数据,所以它将会比较预期数据和原始数据是否一致,如果一致就修改,不一致就修改失败,下文中CAS就是乐观锁的思想,意为比较并替换。
乐观锁会出现ABA的情况,即数据A被改为B,又被改为A,乐观锁无法发现数据被修改,为解决此问题,可以使用版本迭代的方式来检测是否被篡改过数据,在进行修改时版本上升,在进行比较时同时比较数据和版本
CAS在许多的实现下,其操作必须是原子性的,因为其使用在多线程情况下,进行的本质是两个步骤,比较并替换,这个步骤必须被合成一个原子性操作以保证线程安全。在JAVA实现中,常采用四个参数,object,offset,A,B。offset是偏移量,和Object组合获得内存中的lock_flag,A是预期值,B是要更新的数值,以此做到原子操作。
在CAS上,依旧会有lock,这个lock并非JAVA的锁,而是类似总线锁的操作,以确保多CPU下的安全,此与可见性问题有关。
悲观锁的预期是悲观的,它默认会有人修改数据,所以它会先加锁,然后修改数据。
无锁状态的锁标记为01,偏向锁标记为0,因此mark word的后三位为001。
在关闭偏向锁时,新创建的对象都属于无锁不可偏向状态。
在开启偏向锁时,在启动项目的前4秒内,新建的对象都是无锁不可偏向状态,因为jvm会有4秒的偏向锁开启的延迟时间,在此期间,偏向锁不会开启,4秒钟后创建的对象才是具有偏向锁的对象。
为什么要强调是无锁不可偏向状态,为什么偏向锁会延迟启动,其实原因是一致的,因为刚刚开启JVM时,大量对象生成后一定会产生竞争,添加偏向锁的对象性能而会严重下降,既然竞争一定会发生,不如直接添加轻量级锁。
无锁状态可以详细称为无锁不可偏向状态,如果加锁将会直接跳过偏向锁,进入到轻量级锁状态。
匿名偏向锁可以详细称为无锁可偏向状态,因为此时还没有线程抢占到这把锁。
偏向锁的锁标记为01,偏向锁标记为1,因此mark word的后三位为101,在存储hashcode的位置,将会存储ThreadId。
偏向锁属于乐观锁,会在加锁但没有产生竞争的情况下使用,进行一次CAS,在1.6与1.7默认开启的偏向锁在1.8默认关闭。
偏向锁的思想是,记录第一个进入偏向锁的线程ID,当第二个线程进入时,进行线程ID的比较,如果一致就可以继续持有锁,如果不一致,将发生偏向锁的撤销,膨胀或是重偏向等动作。
当一个线程进入到synchronized的代码块时,就会从这个线程当前的栈找到一个空闲的BasicObjectLock(Lock Record列表)。这是一个基础的锁对象,在后续的轻量级锁和重量级锁中都会用到。BasicObjectLock中包含两个属性:
将BasicObjectLock的oop指针指向当前的锁对象
获得当前锁对象的对象头,通过对象头判断是否可以进行偏向。
对于偏向锁,对象头内的信息无非就是三个,ThreadId,偏向锁标记,锁标记,锁标记固定为01,因此只需要查看ThreadId与偏向锁标记即可。
接下来,就需要使用CAS进行判断并比较,此时会发生两种情况
只有两种情况下才能成功替换
当ThreadId为0,偏向锁标记为1,此时为匿名偏向锁。在关闭jvm偏向锁延迟或4秒后创建的对象将会具有偏向锁,但是此时并无线程可以持有这个锁,因此此时的偏向锁为匿名偏向锁。匿名偏向锁是偏向锁的初始状态。
批量重偏向放在之后讲解。
如果不符合这两种情况,CAS操作会失败,证明当前线程存在竞争,替换失败后,会执行偏向锁撤销操作。偏向锁撤销需要等到全局安全点,在全局安全点,所有线程都会暂停,此时获得偏向锁的线程会被挂起。
在安全点,所有线程都会被遍历,检查持有偏向锁的线程是否保持存活。
完成操作后,从安全点继续执行代码。
代码执行完成,退出同步块,同步锁释放。此时线程将会逐条删除记录,释放Lock Record,但是需要注意,所谓的偏向锁释放并不是真正的释放,线程ID依然保存在ThreadID之内,只是记录消失,该偏向锁依旧偏向此线程。
向锁升级为轻量级锁之后,执行完同步代码块,锁被释放,对象状态将变为无锁不可偏向状态,即001,符合锁只要升级膨胀就不能回退的要求。
偏向锁升级为重量级锁也是可能的。
在未禁用偏向锁的情况下,假设一个线程针对大量对象设置了偏向锁,之后其他线程来访问这些对象,在不触发锁竞争的情况下,也需要对这些对象进行偏向锁的撤销和锁升级膨胀,因此虚拟机会认为,此时对象的偏向是有问题的,当偏向锁针对一个线程的撤销发生20次之后,就会触发批量重偏向,将其余被访问对象的偏向锁重新指向新线程,即前19次的偏向锁依旧会发生偏向锁撤销和膨胀为轻量级锁,第20个之后被访问的对象就会直接重偏向至新线程,未被新线程访问的对象偏向锁保持不变。
当此对象的偏向锁继续被撤销,达到40次之后,就会触发批量撤销,JVM认为该class的使用场景存在多线程竞争,将其余具有偏向锁的对象执行偏向锁撤销,同时标记该class为不可偏向,之后再创建此class的对象就直接为无锁不可偏向状态,即直接走轻量级锁的逻辑。
下面是几个重偏向的参数
在JVM中,以class为单位,为每一个class维护了一个偏向锁撤销计数器,当这个对象发生偏向锁撤销时,计数器会进行累加,当超过阈值,就会触发批量重偏向。
一般情况下,class中的epoch与对象的epoch是一致的,当发生批量重偏向时,首先会将class的epoch值+1,接着遍历所有当前活着的的线程的栈,找到该class所有正处于偏向锁状态的锁实例对象,将epoch值修改为新值。此时其他未被线程持有的锁对象epoch会比class的epoch小,在其他线程获取到该锁对象时,会尝试使用CAS执行替换重偏向。
距离上次批量重偏向的25秒内,如果撤销计数达到40,就会发生批量撤销,如果超过25秒,那么就会重置计数。
如果超过25秒没有达到批量撤销累加计数,证明上次的批量重偏向效果显著,将会重置计数器计数,如果超过了,证明这个class不适合偏向锁。
轻量级锁的锁标记为00,因此mark word的后两位为00。
轻量级锁属于乐观锁,会在共享资源被抢占的时候使用,当线程发现锁已经被抢占,将导致该线程的锁膨胀,成为重量级锁。
当一个线程进入到synchronized的代码块时,就会从这个线程当前的栈找到一个空闲的BasicObjectLock(Lock Record列表),在BasicLock中有一个成员属性markOop_displaced_header,这个属性专门用来保存对象的原始对象头Mark Word。
将对象的无锁状态Mark Word保存到_displaced_header中。
检测是否为无锁状态,如果是,通过CAS将对象的Mark Word 替换为指向Lock Record 的指针,如果替换成功,就表示抢占成功。
如果CAS失败,就表示此对象不为无锁状态,需要判断对象指向Lock Record 的指针
轻量级锁的释放同样使用了CAS操作。
尝试将Lock Record的displaced_header存储的 mark word 替换回对象mark word,此操作使用CAS进行,这时需要检查锁对象的mark word中lock record指针是否指向当前线程的锁记录:
为什么会释放失败?因为轻量级锁在被某个线程占有时,会被其他线程尝试抢占,如果无法获得轻量级锁,就会触发锁膨胀。锁膨胀的逻辑是抢占线程会在判定此时为轻量级锁的情况下,修改锁对象的Mark Word,设置状态为inflating状态。这个操作是通过自旋实现的,多个线程触发膨胀也只会有一个线程修改状态。
因为被修改了状态,所以轻量级锁释放必然会导致失败,对象的锁升级膨胀为重量级锁,其他线程也因为是重量级锁而被阻塞,所以释放时还要唤醒被阻塞的线程。
轻量级锁的设计集中在Lock Record属性markOop中存储的锁对象Mark Word和锁对象头存储的指向Lock Record的指针,这样设计的目的是因为,轻量级锁一定是有多个线程进行竞争的,锁对象在竞争时可能会发生状态的变化,但是Lock Record中存储的Mark Word肯定不会发生变化,这样通过对比Lock Record与锁对象的Mark Word,就可以判断锁对象是否被其他线程抢占过,如果有,就要在释放轻量级锁的过程中唤醒被阻塞的线程。
轻量级锁和偏向锁的重入计数是很类似的,所以在这里详细讲述一下
在线程的Lock Record中储存着锁对象的Mark Lock和指向锁对象的owner,但这是首次分配时保存的。
之后再有的Lock Record中只保存指向锁对象的owner,displaced mark word为null
因此重入计数就由Lock Recod的数量来表示,需要解锁就删除一条,直到只剩下最初的那一条时,进行常规解锁操作。
轻量级锁的锁标记为10,因此mark word的后两位为10。
重量级锁属于悲观锁,非公平锁(允许插队),线程进入阻塞状态。每一个java对象都有一个monitor,每一个线程都有一个监视器Monitor Record,当线程想要获取一个加锁资源时就必须获取到它的monitor,然后将所有权据为己有,直到线程运行完毕才会释放所有权,唤醒被阻塞的线程抢占该资源。准备抢占锁的线程会进入同步队列,没有抢占到资源的线程将进入阻塞状态。
获取重量级锁之前,要先进行锁膨胀。锁膨胀需要创建一个对象ObjectMonitor,然后把ObjectMonitor对象的指针保存到锁对象的Wark Word之中。锁膨胀分为四种情况:
以上过程都是通过自旋完成的,避免了线程竞争导致CAS失败的问题,因此轻量级锁没有自旋,在锁膨胀之后的自旋也是在重量级锁中实现的,只有重量级锁有自旋。
锁膨胀完成后,锁对象的Mark Word会保存指向ObjectMonitor的指针,重量级锁的竞争都在ObjectMonitor中完成,下面介绍一些ObjectMonitor中常见的字段
接下来就是重量级锁的获取,很简单
重量级锁的释放同样非常简单,当同步代码块执行完毕后,会触发重量级锁的释放
park()方法是用来阻塞竞争队列线程的方法,需要注意的是,park()方法是需要系统调用完成的,用户态无法完成系统调用,因此会发生用户态到内核态之间的切换,重量级锁消耗性能的主要原因就是这个。
同步锁存在降级吗?存在的。
在全局安全点,执行清理任务的时候会触发尝试降级锁
jdk1.6中对锁的实现引入了大量的优化