说一下Synchronized?
- Synchronized锁是Java中为了解决线程安全问题的一种方式,是一种悲观锁
- Synchronized可以用来修饰方法或者以代码块,用来保证线程执行方法或代码块时的原子性
- Java中任何一个类的对象都可以用来作为锁对象,但是如果我们希望解决线程安全问题的话,那就必须要保证这些引发安全问题的线程使用的是同一把锁
- 同步代码块的锁对象需要我们自己指定,一般如果是static方法的话,我们一般使用类的class对象作为锁对象,如果是非static方法的话,我们一般使用this作为锁对象;同步方法的锁对象不需要我们自己指定,如果同步方法是static的,则使用类的class对象。如果是非static的则使用this作为锁对象
- 因为使用Synchronized锁会导致线程串行执行,所以建议其锁定的范围越小越好
Synchronized锁具体存放在什么位置?
Synchronized锁具体存放在对象头的MarkWord中
在Java中,一个对象由三个对象组成,分别是对象头、实例化数据和对齐填充字节,而对象头又包括三部分,分别是MarkWord、类型指针、数组长度(只有数组类型有)
以64位操作系统系统为例,MarkWord的内存结构如下:
说一下自旋锁?
所谓自旋锁,就是线程在获取锁失败时,不会立即进入阻塞状态,而是会尝试等待一段时间,由于这个等待是通过执行一段无意义的循环来完成的,故名自旋锁,如果线程自旋多次仍然没有获取到锁,才会进入阻塞状态。
如果没有自旋锁,那么线程一旦获取锁失败就会被阻塞,由于线程的阻塞和唤醒需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,而且在很多应用中,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
我们可以通过JVM参数开启或关闭自旋锁,或者设置自旋的默认次数。
JDK1.6引入自适应自旋锁,所谓自适应就意味着自旋的次数不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
什么是锁消除?
有些情况下,JVM检测到同步方法或同步代码块不可能存在共享数据竞争,这时JVM会对其同步锁进行锁消除,锁消除是在编译阶段完成的,JDK1.8中默认开启
说一下Synchronized的锁升级?
为了减少获得锁和释放锁而带来的性能消耗,Synchronized锁由低到高分为四种状态,分别是:无锁、偏向锁、轻量级锁、重量级锁,状态间的转换会随着竞争情况逐渐升级。除了偏向锁可以降级为无锁以外,锁只能升级但不能降级。
偏向锁
偏向锁引入的初衷是为了解决轻量级锁在没有竞争时每次重入仍需要CAS的问题,当一个线程试图获取偏向锁时,会首先对比偏向锁中记录的线程ID是否是自己,如果不是则会尝试通过一次CAS将自己的线程ID记录到锁对象的MarkWord中,一旦记录成功,那么之后无论是锁重入还是该线程再次获取到锁,都只需要比对线程ID是否是自己即可,不用重新 CAS。一般来说,如果不发生竞争问题的话,这把锁就归该对象所有了,故名偏向锁
如果我们开启了偏向锁(默认开始),那么使用Synchronized加锁时最开始其实就是偏向锁,但是偏向锁默认是延时生效的,如果我们希望立即生效也可以通过参数来设置
当偏向锁被撤销时会变成轻量级锁,偏向锁在以下情况时会被撤销:
- 调用锁对象的hashCode方法:对象的hashCode也是存放在MarkWord中的,而且是在使用到的时候才会被加载,但是如果该对象是处于偏向锁状态的,那么它的MarkWord中是没有空间来存放hashCode的(原本存放hashCode的空间被存放线程ID的空间挤没了),故当我们调用锁对象的hashCode方法时,为了保存这个对象的hashCode值,偏向锁会撤销为轻量级锁
- 锁竞争:当一个线程试图获取偏向锁,但是CAS失败时,就认为发生了锁竞争,这时候就会对该偏向锁进行撤销
- 调用锁对象的wait/notify方法:wait/notify方法需要与底层操作系统打交道,因此无论是对象无论是在偏向锁状态下还是在轻量级锁状态下,调用wait/notify方法都需要升级成重量级锁
- 批量撤销:如果一个锁对象撤销偏向锁的次数超过40次,那么JVM会把这个对象所对应的类的所有对象都撤销偏向锁,并且这个类新实例化的对象也是不可偏向的。这个操作其实就是禁用了这个类的可偏向属性
接下来讲一下偏向锁在撤销时会发生什么:
-
偏向锁的撤销需要等待一个全局安全点(Safe Point,在这个时间点上没有任何字节码正在执行,即Stop The World)
-
检查持有偏向锁的线程A是否存活
-
如果仍存活,且正在执行同步代码块,则偏向锁直接升级为轻量级锁,且线程A获得该轻量级锁
-
如果未存活,或未在执行同步代码块,则校验该锁是否允许重偏向
-
如果不允许重偏向,则将Mark Word设置为无锁状态
-
如果允许重偏向,则线程A释放偏向锁,偏向锁将被设置为匿名偏向状态
-
当一把锁被撤销次数达到二十次时,jvm就会认为”自己的偏向可能出现了问题“,这时就会允许重偏向,即将该偏向锁设置为匿名偏向状态,如果锁撤销次数未达到二十次(应该)是不允许重偏向的
匿名偏向状态也就是偏向锁还没有被任何线程获取时的状态,这个状态下线程可以通过CAS将线程ID记录到MarkWord中
-
轻量级锁
轻量级锁与偏向锁一样,同样是适用于锁没有竞争的场景下,线程获取偏向锁时是往锁对象的MarkWord中记录自己的ID,但在获取轻量级锁时进行的操作要复杂许多
假如说线程A现在要获取轻量级锁,那么具体过程如下:
- 线程A检查该锁的锁标志位是否为01,也就是判断锁状态是否为无锁,如果是则进入第二步,如果不是则进入第三步
- 在线程A的栈帧中创建一个名为锁记录(Lock Record)的空间,并把该锁对象的Mark Word拷贝一份到锁记录中,称之为Displaced Mark Word,然后线程A尝试通过CAS将锁记录的指针保存到锁对象的Mark Word中,并让锁记录的owner指针指向锁对象,如果能执行成功,那么此时加锁就成功了,锁标志位由01变成00,锁对象进入轻量级锁定状态;如果执行失败,则进入第三步。
- 线程A会去检查锁对象的Mark Word中是否保存了当前线程锁记录的指针,如果保存了,则说明出现了锁重入,这时会再在栈帧中创建一个锁纪录,假设这个锁记录叫锁记录B,之前的锁记录叫锁记录A,那么锁记录A与锁记录B的不同点在于,锁记录B不保存锁对象的Mark Word,也就是Displaced Mark Word为null,另外需要说明,轻量级锁重入的计数就是通过统计锁记录的个数得来的;如果没有保存,则进入第四步
- 线程A获取轻量级锁失败,一怒之下将该轻量级锁膨胀为重量级锁,锁标志位由00变成10,然后线程A会进行自旋来尝试获取锁(注意这个时候已经是重量级锁了),当自旋达到一定次数后,线程A进入该锁的阻塞队列
线程A释放轻量级锁的流程如下:
- 判断锁记录中的Displaced Mark Word是否为null,如果是,则说明是锁重入,移除owner的指向,不做替换操作;如果不是,则进入第二步
- 通过CAS把锁记录中的Displaced Mark Word与锁对象的对象头中的锁记录指针进行替换,如果替换成功,则轻量级锁解锁成功,该锁变成无锁状态;如果替换失败,则说明发生了锁膨胀,该对象现在处于重量级锁定状态,线程A进入重量级锁的解锁流程
重量级锁
重量级锁需要使用Monitor来完成,Monitor可以翻译为监视器或者管程,在HotSpot虚拟机中,Monitor是基于C++的ObjectMonitor类实现的,每个Java对象都可以关联一个Monitor对象,当我们把该对象作为重量级锁使用时,该对象的Mark Word中就会被设置一个指向Monitor对象的指针。Monitor对象中重要的属性有以下几个:
- owner:指向持有锁的线程的锁记录
- waitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
- entryList:存放处于等待锁block状态的线程队列
- recursions:记录锁重入次数
仍然使用轻量级锁中的例子,线程A获取轻量级锁失败,轻量级锁膨胀为重量级锁,那么这中间发生了什么事呢?简单来说如下:
- 为锁对象申请Monitor对象,并将Monitor对象的地址设置到锁对象的Mark Word中
- 在Monitor的owner属性中设置持有锁线程的锁记录指针,锁记录仍指向锁对象的Mark Word,这一点保持不变
- 线程A自旋仍未获取锁,进入Monitor的entryList中
那么假如在上述案例中,轻量级锁的持有者是线程B,那么线程B释放锁时,进行的流程简单来说如下:
- CAS失败,发现锁已经膨胀为了重量级锁
- 通过锁对象的Mark Word找到Monitor对象的地址, 将其owner设置为null(这是在锁没有重入的情况下;若存在锁重入则recursions减一,且不执行下一步)
- 唤醒entryList中的线程,进行锁的竞争
那么假如线程A需要竞争该锁,那么它做的事情如下:
- 尝试通过CAS试修改Monitor的owner属性,若修改成功则表示获取锁成功,若修改失败则进入第二步
- 判断该锁的持有者是否为线程A,若是则线程A重入该锁,recursions加一,若不是则进入第三步
- 线程A进行自旋,达到一定次数后仍未获取锁,则再次进入entryList
entryList中的线程竞争锁时是非公平的,意思就是说先来的线程可能等了很久也获取不到锁,后来的线程可能刚到就获取到锁了