谈谈你对Synchronized的理解
synchronized是Java提供的原子性内置锁,也被称为监视器锁。使用synchronized以后,同步代码块前后就会加上moniterenter和moniterexit字节码指令,它依赖于操作系统底层互斥锁实现,synchronized的主要作用就是实现原子性操作和解决了共享变量的内存可见性问题。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁以后,该对象头的Mark Word就被设置为指向Monitor对象的指针。
- Thread0 首先得到锁,将Owner设置为Thread0,由于资源不足,Thread0 执行wait(),进入WaitSet中,同时释放锁,将Owner置为null。
- Thread1、Thread2、Thread3竞争锁,Thread1拿到锁,Thread2、Thread3进入EntryList阻塞。Monitor将Owner置为Thread1,Thread1执行同步代码块中的逻辑,执行结束,释放锁,将Owner置为null,并且唤醒了Thread0。
- Thread0进入EntryList,和Thread2、Thread3非公平性的竞争锁。
锁的升级和优化机制了解吗?
在Java SE1.6之后,锁一共有四个状态,按照级别从低到高为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级,但不能降级。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获取,为了降低获取锁的代价,引入了偏向锁。
获取偏向锁:
当一个线程获取到锁,会在对象头和栈帧中的锁记录里存储锁偏向的线程id,以后该线程进入和退出同步块不需要进行CAS操作来加锁和解锁,只需要测试一下对象头的Mark Word是否存储着指向对当前线程的偏向锁。测试成功,表示已经获得了锁;测试失败,则还需要测试偏向锁的标识是不是1,如果没有设置,则使用CAS竞争锁;如果设置了,尝试使用CAS将对象头的偏向锁指向当前线程。
撤销偏向锁:
首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否存活,若果线程已经结束,则将对象头设置为无锁状态;如果,线程还存活,遍历偏向对象的锁记录,锁记录和Mark Word要么重新偏向于其它线程,要么恢复到无锁状态,最终唤醒暂停的线程。
轻量级锁
轻量级锁加锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,称为Displaced Mark Word。然后线程尝试使用CAS将对象头的Mark Word替换为指向锁记录的指针。如果成功,则表示获取锁成功;否则,表示有竞争,当前线程通过自旋来获取锁。
轻量级锁解锁:
使用CAS将Displaced Mark Word替换到对象头中,成功,则表示没有竞争发生;否则,锁存在竞争,锁就会膨胀到重量级锁。
简单来说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
你知道ReentrantLock和Synchronized有什么区别吗?
Synchronized可以修饰实例方法,静态方法,代码块,自动释放锁。
ReentrantLock一般需要try catch finally语句,在try中获取锁,在finally释放锁,需要手动释放锁。
他们都是可重入锁。
主要区别:
1:等待可中断:在ReentrantLock中,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,通过lock.lockInterruptibly()来实现,避免了死锁发生。
2:公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参(true)改变。只不过使用公平锁的话会导致性能急剧下降
3:绑定多个条件:Synchronized只有一个等待队列,ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock原理你知道吗?
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现,等后面总结到AQS再说。
CAS的原理呢?
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令cmpxchg来保证操作的原子性,它包含三个操作数:
- 变量内存地址,V表示
- 旧的预期值,A表示
- 准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
在sun.misc.Unsafe中实现了CAS,是一个本地方法,底层是c++实现。
public final native boolean compareAndSwapInt(
Object obj,
long offset,
int expected,
int updated);
CAS就是乐观锁的一种实现。
乐观锁:每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量
CAS的缺点主要有3点:
1:ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
解决方法:加版本号或者时间戳,Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
2:循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。适合少量线程的情况下,线程多的话很耗时;
3:只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。