1.为什么需要锁
解决多个线程访问同一个可变的状态变量时的安全问题。
2.内置锁 - synchronized
任何一个非null对象都可以作为锁
。内置锁是可重入的非公平锁。在JVM中,内置锁也叫对象监视器
;
-
使用方法
-
1.作用在方法上,持有对象实例this的锁
public synchronized void doSomething() { //方法体 }
-
2.作用在代码块上,持有对象实例lockObject的锁
public int doSomething(){ synchronized(lockObject) { //一次只能有一个线程进入 } }
-
3.作用在静态方法上,持有类(this.getClass)的锁
public synchronized static void doSomething() { // todo }
-
4.作用在类上,持有类(class)的锁
class ClassName { public void method() { synchronized(ClassName.class) { // todo } } }
-
-
实现原理
synchronized的加锁和释放都是由JVM提供,在软件层面依赖JVM,当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:◆
Contention List
:所有请求锁的线程将被首先放置到该竞争队列
。◆
Entry List
:Contention List中那些有资格成为候选人的线程被移到Entry List。◆
Wait Set
:那些调用wait方法被阻塞的线程被放置到Wait Set。◆
OnDeck
:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck。◆
Owner
:获得锁的线程称为Owner。◆
lock1.jpg!Owner
:释放锁的线程。
新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从ContentionList中移动线程到EntryList,下面说明下ContentionList和EntryList 的实现方式:
- [ ] ContentionList 虚拟队列
ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指 针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行, 通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock- Free的队列.因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。
- [ ] EntryList
EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。
OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不 发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。
- [ ] 自旋锁
那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响 锁的性能
缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法 获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非 常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。
线程在进入等待队列ContentionList时,也即第一步操作前.首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready线程的锁。
- [ ] 偏向锁
在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题.
现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操 作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
- Mark Word
HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.
-
[ ] 未被锁定状态:
lock4.png
-
[ ] 其他状态:
lock3.png
openjdk\hotspot\src\share\vm\oops\oop.hpp下oopDesc类是JVM对象的顶级基类,故每个object都包含markOop。
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;//markOop:Mark Word标记字段
union _metadata {
Klass* _klass;//对象类型元数据的指针
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
public:
markOop mark() const { return _mark; }
markOop* mark_addr() const { return (markOop*) &_mark; }
void set_mark(volatile markOop m) { _mark = m; }
void release_set_mark(markOop m);
markOop cas_set_mark(markOop new_mark, markOop old_mark);
// Used only to re-initialize the mark word (e.g., of promoted
// objects during a GC) -- requires a valid klass pointer
void init_mark();
Klass* klass() const;
Klass* klass_or_null() const volatile;
Klass** klass_addr();
narrowKlass* compressed_klass_addr();
....省略...
}
在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor。
openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源码如下:
ObjectMonitor() {
_header = NULL;//markOop对象头
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
3.显示锁 - Lock
synchronized存在局限性,例如:
- 占有锁的线程执行时间较长,不想让其他线程无期限地等待下去
- 读写锁中,读操作线程之间不发生冲突
- 无法得知线程有没有成功获取到锁
-
共享锁
ReentrantLock使用的是独占锁,Semaphore,CountDownLatch,ReentrantReadWriteLock使用的是共享锁。
独占锁与共享锁的实现大同小异。- 独占锁:
state为0代表有资源,state大于1代表锁已经被其他线程获取。
独占锁是只有头节点获取锁,其余节点的线程继续等待,等待锁被释放后,才会唤醒下一个节点的线程;
独占锁的同步状态state值在0和1之间切换,保证同一时间只能有一个线程是处于活动的,其他线程都被阻塞,参考ReentranLock。当owner线程再次重入时,state在1到N间切换。
独占锁是一种悲观锁。- 共享锁:
state值在整数区间内(自定义实现),如果state值<0则阻塞,否则不阻塞。
共享锁是只要头节点获取锁成功,若剩余资源大于0,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。
共享锁是一种乐观锁,允许多个线程同时访问共享资源。
网友评论