管程
指的是管理共享变量以及共享变量的操作过程,让它们支持并发。
synchronized实现原理
原理
- 组件
- contentionList 请求锁的线程队列
- entryList 有资格的候选者队列
- onDeck 竞争候选者
- waitSet wait方法阻塞的线程队列
- owner 竞争到锁的线程
- !owner 释放锁的线程
-
修饰代码块
由monitorenter和monitorexit指令实现同步,进入monitorenter,线程将持有Monitor对象,退出monitorexit指令后,线程释放Monitor对象。 -
修饰方法
jvm使用ACC_SYNCHRONIZED访问标志来区分是否是同步方法。如果方法设置了该标志,执行线程先持有Monitor对象,然后再执行方法。执行完后线程释放对象,执行过程中其他线程无法获取到Monitor对象。
总结:
多线程同时访问一段同步代码时,会被存放在contentionList和entryList中,处于阻塞状态的线程,都会被加入到该列表中。接下来,当线程获取到对象的Monitor后, 底层是依赖操作系统的Mutex Lock来实现互斥,(java线程映射到内核线程上,每次阻塞或唤醒都不可避免带来用户态和内核态的切换)每次申请成功则持有该Mutex,其他线程无法获取到Mutex,竞争失败的线程会再次进入到contentionList被挂起。
如果线程调用wait方法,就会释放当前持有的Mutex,并且该线程会进入到watiSet集合,等待下一次被唤醒。
如果线程顺利执行完成也会释放Mutex。
锁升级优化
- 偏向锁
用来优化同一个线程申请同一个锁的竞争。当一个线程再次访问这个同步代码块或者方法时,只需要去对象头的Mark Word中判断一下是否由偏向锁指向它的id,无需在进入Monitor竞争对象。
一旦出现其他线程竞争锁资源,偏向锁就会被撤销,在全局安全点检查该线程是否还在执行该方法,如果是升级锁,反之其它线程抢占。
高并发下,大量线程同时竞争同一个锁资源,偏向锁就会撤销,发生stop the world,会带来更大的性能开销。
-XX:-UseBiasedLocking //关闭偏向锁(默认打开)
-XX:+UseHeavyMonitors //设置重量级锁
-
轻量级锁(自旋锁)
当有另外的线程竞争到这个锁时,由于该锁已经是偏向锁,当发现对象头的Mark Word中的线程id不是自己的id,就会进行cas操作获取锁,如果获取成功,直接替换Mark Word中的线程id为自己的id,该锁会保持偏向状态。
如果获取失败,代表当前锁由竞争,偏向锁升级成轻量级锁。适用于线程交替执行同步快的场景,绝大部分的锁在整个同步周期都不存在长时间的竞争。 -
自旋锁和重量级锁
轻量级锁cas抢锁失败,线程就会被阻塞挂起,如果持有锁的线程在很短的时间内又释放了锁的资源,进入阻塞状态的线程又要重新申请锁资源。
jvm提供了一种自旋锁,可以通过自旋的方式不断尝试获取锁,而不用被阻塞挂起。大多数情况下,线程持有锁的时间都不会太长,进入阻塞状态得不偿失。jdk1.7使用了自适应自旋锁,自旋的次数由vm设置。
在竞争不激烈且锁的占用时间不长的情况下,自旋锁可以提高系统性能。但是一旦竞争激烈或者占用时间过程,自旋锁会导致大量线程cas重试状态,占用cpu资源,增加系统性能开销。自旋cas重试之后如果抢锁失败,同步锁就会升级到重量级锁。未抢到锁的线程进入到Monitor,之后阻塞在waitSet中。
JIT实现锁消除和锁粗化
JIT编译器在动态编译同步代码快时,通过逃逸分析,判断锁的对象是否只能被一个线程访问,而没有发布到其它线程,如果是的话,JIT在编译同步块的时候,就不会生成synchronize所表示的锁申请和释放的机器码,消除锁。
同样,如果发现几个相邻的同步块使用的是同一个锁,JIT把几个同步块合并成一个大的同步块,避免线程反复申请,释放同一个锁带来的性能消耗。
减小锁粒度
HastTable基于数组+链表,在竞争激烈的场景下,性能存在瓶颈。而ConcurrentHashMap使用了分段锁Segment降低锁资源竞争。
面试题加餐
-
偏向锁和轻量级锁的区别
1)轻量级锁通过cas操作,避免进入互斥操作;
2)偏向锁是在完全无锁的场景,cas也不执行。 -
自选锁升级到重量级锁的条件
1)自旋次数超过10次;
2)自旋线程超过系统core数的一半。 -
wait和notify/notifyAll为什么要放在同步代码块中执行?
每个对象都有一个Monitor对象,加锁就是竞争Monitor对象,Monitor的c++实现中有一个waitSet集合,处于wait方法的线程都会被加入到这个集合中,在执行notify或notifyAll唤醒时,会重新进入到entryList队列中。
不在synchronize同步代码块中调用wait和notify方法是不会触发Monitor对象的这些操作,没有意义。
- synchronize为什么不是公平的?
对于entryList中的线程会先执行cas操作获取锁,如果monitorenter竞争失败,就会重新进入到contionList中,这对先进入队列的线程是不公平的。其次,自旋锁还会抢占onDeck线程的锁资源。
- 简述锁升级过程
当一个线程获取锁时,首先对象锁将成为一个偏向锁,为了优化同一个线程重复获取导致的用户态和内核态的切换问题;其次,如果有多个线程竞争锁资源,锁将会升级成轻量级锁,适用于短时间竞争不激烈的场景,轻量级锁使用了自选锁来避免线程用户态和内核态的频繁切换,提高了系统性能,如果锁竞争太激烈,同步锁就会升级为重量级锁。
减少锁竞争,是优化synchronize同步锁的关键,尽量使用偏向锁或者轻量级锁,还可以减小锁粒度来降低锁竞争,通过降低锁的持有时间来提高锁在自旋是获取锁资源的成功率,避免升级为重量级锁。
ReentrantLock原理(CAS+AQS)
原理
Lock锁需要显示的获取和释放锁,jvm是隐式。
AQS类结构基于链表实现的等待队列,存储阻塞的线程。volatile int类型的state变量,表示加锁状态。通过cas操作获取锁,如果已经有线程获取了锁,就会加入到AQS队列并被挂起;当前线程释放锁,AQS队列的队首线程就会被唤醒再次尝试获取锁。
对于公平锁,没有获取到锁就会排到队尾,按队列顺序获取锁。如果是非公平锁,还会有其他的线程来尝试获取锁,可能会让这个线程获取成功。
AQS原理: 1.Node类结构有一个双向链表的同步队列,通过控制state变量,来判断锁的状态,对于非可重入锁不是0则阻塞;2.对于可重入锁,判断当前线程是否已经获取到锁,如果是state+1,当释放锁的时候,同样需要释放到state=0,其它线程才有资格获取锁。3.AQS有两种资源共享方式,X锁,ReentrantLock,S锁,Semaphore,CountDownLatch,ReadWriteLock,CyclicBarrier。
//todo AQS的其它组件