java中锁的底层实现

https://blog.csdn.net/qq_29753285/article/details/81299509

锁:synchronized 和 reentrantlock
一、synchronized
1、CAS(compare and swap)
为了提高性能,JVM很多操作都依赖CAS实现,一种乐观锁的实现。本文锁优化中大量用到了CAS,故有必要先分析一下CAS的实现。
CAS:Compare and Swap。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。如果A=V,那么把B赋值给V,返回V;如果A!=V,直接返回V。
2. CAS的目的
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,
因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
3. CAS存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

Java中的Monitor
java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronized methods )被多个线程调用时,
该对象的monitor将负责处理这些访问的并发独占要求。

当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;
如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。

 locked_value = 0,//00偏向锁 
 unlocked_value = 1,//01无锁 
 monitor_value = 2,//10监视器锁,又叫重量级锁 
 marked_value = 3,//11GC标记 
 biased_lock_pattern = 5//101偏向锁

synchronized关键字修饰的代码段,在JVM被编译为monitorenter、monitorexit指令来获取和释放互斥锁。

monitorenter	:获取锁
monitorexit	:判断锁是否存在

偏向锁的获取 ;fast_enter(); 再fast_enter()方法中判断是否开启了偏向锁,如果未开启则调用非偏向锁:slow_enter();

偏向锁这个函数定义了获取偏向锁的流程,具体逻辑总结:
1. 判断当前对象是否为可偏向(101),且偏向时间戳已过期(没有其他线程在占用该对象),如果是,则进入步骤2,否则进入步骤3
2. 执行CAS操作将markword中的线程ID替换为本线程ID。如果成功则进入步骤4,否则进入步骤3
3. 存在竞争,当达到全局安全点(safepoint),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级,升级完成后被阻塞在安全点的线程继续执行同步代码块;
4. 执行同步代码

偏向锁的撤销 : revoke_at_safepoint
只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,偏向锁的撤销由BiasedLocking::revoke_at_safepoint方法实现:

偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,
然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,
拥有偏向锁的栈会被执行,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,
或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。

偏向锁在Java 1.6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,
如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false参数关闭偏向锁

轻量级锁的获取:slow_entry
如果关闭了偏向锁功能,则会直接再monitorenter方法中进入slow_enter。否则,在faster_enter中竞争偏向锁导致偏向锁升级为轻量级锁后进入。

轻量级锁的释放:fast_exit
1、确保处于偏向锁状态时不会执行这段逻辑;
2、取出在获取轻量级锁时保存在BasicLock对象的mark数据dhw;
3、通过CAS尝试把dhw替换到当前的Mark Word,如果CAS成功,说明成功的释放了锁,否则执行步骤(4);
4、如果CAS失败,说明有其它线程在尝试获取该锁,这时需要将该锁升级为重量级锁,并释放;

重量级锁的 ;
重量级锁的膨胀过程:ObjectSynchronizer::inflate
膨胀过程的实现比较复杂,大概实现过程如下:
1、整个膨胀过程在自旋下完成;
2、mark->has_monitor()方法判断当前是否为重量级锁(上图18-25行),即Mark Word的锁标识位为 10,如果当前状态为重量级锁,执行步骤(3),否则执行步骤(4);
3、膨胀过程已经完成:mark->monitor()方法获取指向ObjectMonitor的指针,并返回。
4、当前锁处于膨胀中(上图33-37行),说明该锁正在被其它线程执行膨胀操作,则当前线程就进行自旋等待锁膨胀完成,
这里需要注意一点,虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起;如果其他线程完成锁的膨胀操作,则退出自旋并返回;
5、当前是轻量级锁状态(上图58-138行),即锁标识位为 00,膨胀过程如下:
通过omAlloc方法,获取一个可用的ObjectMonitor monitor,并重置monitor数据;
通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,标识当前锁正在膨胀中,如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成;
如果CAS成功,设置monitor的各个字段:_header、_owner和_object等,并返回;
6、无锁(中立,上图150-186行),重置监视器值;

重量级锁的竞争:ObjectMonitor::enter
1、通过CAS尝试把monitor的_owner字段设置为当前线程;
2、如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ,记录重入的次数;
3、如果之前的_owner指向的地址在当前线程中,这种描述有点拗口,换一种说法:之前_owner指向的BasicLock在当前线程栈上,说明当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程,该线程成功获得锁并返回;
4、如果获取锁失败,则等待锁的释放;

monitor等待
monitor竞争失败的线程,通过自旋执行ObjectMonitor::EnterI方法等待锁的释放,EnterI方法的部分逻辑实现如下:
1、当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
2、在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中;
3、node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒,实现如下:
4、当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁,TryLock方法实现如下:

monitor释放
当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,
通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor::exit方法中。

二、锁Lock
https://blog.csdn.net/u011109589/article/details/80242931
经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer
锁实现(加锁)

简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,
但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,
而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。

2.1 Sync.nonfairTryAcquire
nonfairTryAcquire方法将是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。
该方法会首先判断当前状态,如果c0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。
如果发现c
0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁。
如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很显然这个Running线程并未进入等待队列。
如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,
也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮

	final boolean nonfairTryAcquire(int acquires) {  
		final Thread current = Thread.currentThread();  
		int c = getState();  
		if (c == 0) {  
			if (compareAndSetState(0, acquires)) {  
				setExclusiveOwnerThread(current);  
				return true;  
			}  
		}  
		else if (current == getExclusiveOwnerThread()) {  
			int nextc = c + acquires;  
			if (nextc < 0) // overflow  
				throw new Error("Maximum lock count exceeded");  
			setState(nextc);  
			return true;  
		}  
		return false;  
	}  
	
2.2 AbstractQueuedSynchronizer.addWaiter	
	addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾:
	private Node addWaiter(Node mode) {  
		Node node = new Node(Thread.currentThread(), mode);  
		// Try the fast path of enq; backup to full enq on failure  
		Node pred = tail;  
		if (pred != null) {  
			node.prev = pred;  
			if (compareAndSetTail(pred, node)) {  
				pred.next = node;  
				return node;  
			}  
		}  
		enq(node);  
		return node;  
	}  

其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:
如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail
如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail

	下面是enq方法:
	    private Node enq(final Node node) {  
			for (;;) {  
				Node t = tail;  
				if (t == null) { // Must initialize  
					Node h = new Node(); // Dummy header  
					h.next = node;  
					node.prev = h;  
					if (compareAndSetHead(h)) {  
						tail = node;  
						return h;  
					}  
				}  
				else {  
					node.prev = t;  
					if (compareAndSetTail(t, node)) {  
						t.next = node;  
						return t;  
					}  
				}  
			}  
		}  

该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。
总而言之,addWaiter的目的就是通过CAS把当前线程追加到队尾,并返回包装后的Node实例。

把线程要包装为Node对象的主要原因,除了用Node构造供虚拟队列外,还用Node包装了各种线程状态,这些状态被精心设计为一些数字值:
SIGNAL(-1) :线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark)
CANCELLED(1):因为超时或中断,该线程已经被取消
CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
PROPAGATE(-3):传播共享锁
0:0代表无状态

2.3 AbstractQueuedSynchronizer.acquireQueued
acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,
如果重试成功能则无需阻塞,直接返回

    final boolean acquireQueued(final Node node, int arg) {  
		try {  
			boolean interrupted = false;  
			for (;;) {  
				final Node p = node.predecessor();  
				if (p == head && tryAcquire(arg)) {  
					setHead(node);  
					p.next = null; // help GC  
					return interrupted;  
				}  
				if (shouldParkAfterFailedAcquire(p, node) &&  
					parkAndCheckInterrupt())  
					interrupted = true;  
			}  
		} catch (RuntimeException ex) {  
			cancelAcquire(node);  
			throw ex;  
		}  
	}  

仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,
奥秘在于第12行的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。

	private final boolean parkAndCheckInterrupt() {  
		LockSupport.park(this);  
		return Thread.interrupted();  
	}  

如前面所述,LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,
还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
			  int ws = pred.waitStatus;  
			  if (ws == Node.SIGNAL)  
				  /* 
				   * This node has already set status asking a release 
				   * to signal it, so it can safely park 
				   */  
				  return true;  
			  if (ws > 0) {  
				  /* 
				   * Predecessor was cancelled. Skip over predecessors and 
				   * indicate retry. 
				   */  
		   do {  
					node.prev = pred = pred.prev;  
			} while (pred.waitStatus > 0);  
					pred.next = node;  
			} else {  
				  /* 
				   * waitStatus must be 0 or PROPAGATE. Indicate that we 
				   * need a signal, but don't park yet. Caller will need to 
				   * retry to make sure it cannot acquire before parking.  
				   */  
				  compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
			  }   
			  return false;  
		  }  

检查原则在于:
规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。

你可能感兴趣的:(java基础)