java并发包的基石:AbstractQueuedSychronier及synchornized

Posted 我爱看明朝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发包的基石:AbstractQueuedSychronier及synchornized相关的知识,希望对你有一定的参考价值。

java并发包的基石:AbstractQueuedSychronier

简介

AQS: AbstractQueuedSychronizer(抽象的队列同步器)是java的j.u.c包中Lock、Semaphore、ReentrantLock等这些锁都是基于AQS框架实现的。

AQS有两种模式:
1. 独占模式ReentrantLock 一次只有一个线程可以竞争到锁
2. 共享模式 CountDownLatch 一次可以多个线程获取到资源

核心

AQS核心数据结构:

 // CAS: compare and set 共享资源通过cas操作来保证并发修改的正确性
 // state有两种共享方式:1. exclusive独占锁 ReentrantLock 2.share共享锁 CountDownLatch
 volatile int state;
 // 同步队列
 FIFO CHL;
 // 当前独占锁占有的线程
 Thread exclusiveOwnerThread;

自定义同步器

不同的自定义同步器竞争共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源state的获取和释放即可,至于具体线程等待队列的维护(如获取资源失败入队,唤醒出队等), AQS已经在顶层实现好了,自定义同步器实现时主要实现以下几种方法:

// 该线程是否正在独占资源
isHeldExclusively();
// 独占方式: 尝试获取资源,成功返回true,失败返回false
tryAcquire(int);
// 独占方式,释放资源,成功返回true,失败返回false
tryRelease();
// 共享方式,尝试获取资源,负数表示失败,0表示成功,但是没有剩余资源;正数表示成功,且有剩余资源
tryAcquireShared(int);
// 共享方式,尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false
tryReleaseShared(int)

ReentrantLock

ReentrantLock reentrantLock = new ReentrantLock();
//加锁
reentrantLock.lock();
//释放锁
reentrantLock.unlock();

加锁

lock & unlock

public void lock() 
 sync.lock();


final void lock() 
 acquire(1);


public final void acquire(int arg) 
 // tryAcquire获取到锁 直接返回
 // tryAcquire没有获取到锁,acquireQueued当前线程加入等待队列
 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
       selfInterrupt();

tryAcquire

fairSync 公平锁

protected final boolean tryAcquire(int acquires) 
 final Thread current = Thread.currentThread();
 int c = getState();
 if (c == 0) 
 	//  当前没有线程获取到锁
 	// cas compare and set 
   	if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
		//  设置当前线程获取到锁
		setExclusiveOwnerThread(current);
		return true;
	
 
 //  可重入锁,当前线程再次获取到锁
 else if (current == getExclusiveOwnerThread()) 
 	 if nextc = c  + acquires;
 	 if (nextc < 0) throw new Error("Maximum lock count exceeded");
 	 	// 设置 state值,为当前重入次数
 	 	setState(nextc);
 	 	return true;
 
 // 无法获取到锁
 return false;

acquireQueued

final boolean acquireQueued(final Node node, int arg) 
// 标记是否获取到锁资源
boolean failed = true;
try 
	// 标记等待过程中是否被中断
	boolean interrupted = false;
	//自旋锁
	for (;;) 
		// 当前尝试获取锁的节点的前一个节点
		final Node p = node.predecessor();
		// 如果当前进程时队列中的老二,那么便可尝试获取锁(等待第一个线程释放锁就可以获取到锁了)
		if (p == head && tryAcquire(arg)) 
			// 可以获取到锁,将head指向该节点
			setHead(node);
			p.next = null;
			// 表示成功获取到锁了
			failed = false;
			//返回等待过程中是否被中断过
			return interrupted;
		
		// 是否可以被挂起,挂起后等待唤醒
		if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())			   
		
		  // 有被中断过
		  interrupted = true;
		
	

 finally
	// 自旋锁没有获取到锁,可能是超时中断,那么可以取消节点在队列中的等待
	if (failed) 
		cancelAcquird(node);
	

isHeldExclusively

//该线程是否正在独占锁
protected final boolean isHeldExclusively()
	return getExclusiveOwnerThread() == Thread.currentThrea();


class AbstractOwnableSynchronizer
	private transient Thread exclusiveOwnerThread;
	
	// 获取当前获取锁的线程
	protected final Thread getExclusiveOwnerThread()
		return exclusiveOwnerThread;
	

释放锁

public void unlock() 
	sync.release(1);


public final boolean release(int arg) 
	if (tryRelease(arg)) 
		// 表示锁已经完全释放
		Node h = head;
		if (h != null && h.waitStatus != 0) 
			unparkSuccessor(h);
		return true;
	
	return false;


protected final boolean tryRelease(int release) 
	int c = getState() - releases;
	// 当前线程是不是获取到锁的线程 
	if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitirStateException();
	boolean free = false;
	// state等于0比表示占领锁的线程已经全部(重入锁)被释放了
	if (c == 0) 
		free = true;
		//exclusiveOwnerThread 设置为null
		setExclusiveOwnerThread(null);		
	
	setState(c);
	return free;

java中的一些锁

synchornized

synchornized关键字是同步锁,可以加在对象、类、方法、代码块,静态方法;Sychornized是可重入的锁。

对象锁: 对象、方法、代码块
类锁:类、静态方法

类锁

public Test
	public void demo() 
		// 同一个jvm只有一个线程某刻可以获取到锁
		sychornized(Test.class) 
			// do something
		
	

静态方法

public Test 
	// 同一个jvm只有一个线程某刻可以获取到锁
	public static sychornized void demo() 
		// do somethig
	
	

对象

public Test 
	public void demo() 
		// 同一个Test实例只有一个线程可以获取到锁
		sychornized(this) 
			// do something
		
	


pulic void demo1() 
	Test test1 = new Test();
	Test test2 = new Test();
	// test1与test2可以同时获的锁,因为他们获得是两把锁
	test1.demo();
	test2.demo();

方法

	pubic Test
		//  同一个Test实例只有一个线程某刻可以获取到锁
		public sychornized void demo()
			// de something
		
	

代码块

 public Test
	// 同一个Test实例只有一个线程某刻可以获取到锁
	pulic void demo(Test test)
		sychornized(test) 
		 // do something
		
	

原理

当代码块使用sychornized编译后会在代码中加入监视器Monitor。

编译后的代码:

monitorenter
// 代码块编译后额命令
monitorexit

当方法声明使用sychornized时,方法编译后会设置ACC_SYNCRONIZED来标识;
当方法调用时,设置指针将会检查方法的ACC_SYNCRONIZED是否被设置了,如果被设置了,执行线程将先持有monitor,然后在执行方法,最后方法完成时释放 monitor。

sychornized是可重入的锁,因此当持有的锁再一次获取到锁,monitor的计数器仍然会加1.每一次释放monitor计数器会减一。

sychornized的锁升级过程: 无锁偏向锁-------> 轻量级锁(自旋锁) ---------> 重量级锁

偏向锁:实际无竞争,只有一个线程在获取锁,在对象的mark word通过cas记录owner为当前线程,owner的值一定会从null到有值的。

轻量级锁:自旋的获取锁,不需要进行线程切换,适合持有锁时间短,竞争不激烈,也是通过cas设置owner的值,多个线程交替短时间内持有锁

重量级锁: 当自旋一段时间,无法获取锁,轻量级锁升级为重量级锁,重量级锁通过 monitor监视器实现。

从这里看起来sychornized重入的实现和AQS的state思想是一致的。

sychornized和ReentranLock的区别

实现上: sychornized是java的关键字,有jvm实现,ReentranLock是API层面提供的。

使用上:sychornized有编译器保证锁的加锁和释放;ReentranLock需要手工声明来加锁和释放锁,忘记释放锁会造成死锁。

性能:sychornized优化后移入了偏向锁、轻量级锁(自旋锁)后两者性能差不多,官方建议如果使用场景一致,建议使用sychornized。

公平非公平锁:sychornized是非公平的,ReentranLock默认是非公平锁,可以设置为公平锁,构造方法传true。

功能区别上:

ReentranLock支持:

  1. 等待可中断 持有锁的线程长时间不释放的时候,正在等待的锁可以选择放弃等待,lockInterruptibly方法

  2. 公平锁, ReentranLock可以实现公平锁

  3. 锁定多个条件 ReentranLock提供了一个条件类Condition,用来实现多个对象分组唤醒线程;sychornized只能要么随机唤醒一个线程要么全部唤醒

两者的实现:
sychornized实现: 重量级锁通过monitor entry 和 monitor exist来加锁代码,偏向锁和轻量级锁通过mark word对owner字段cas设置。

ReentranLock通过AQS实现: state CHL 当前持有锁的线程。

如果选择:
当要使用ReentranLock的三个特性: 等待可中断、公平锁、多条件则选择ReentranLock,否则用sychronized

死锁

两辆车在单行道桥的两端,如果一方不让出路来,那么两俩车都过不了,这就是死锁。

innodb处理死锁的方式: 将持有最少行派他锁的事务进行回滚。
我们在实际开发中,避免死锁:大事务业务上允许插成小事务就插成是小事务。

活锁

两辆这在单行道桥的两端,两方都在给对方让路,那么两辆车都过不去,这就是活锁。

公平与非公平锁

公平锁与非公平锁在AQS中的实现:

  1. 公平锁:当线程想要获取锁,直接插入到CHL队列的队尾,当前线程释放了锁state等于0,则从队列队首获得锁,队列从队首到队尾依次获得。
  2. 当线程想要获取锁,参与竞争锁,如果竞争到则获的锁,没有竞争到,插到CHL队列队尾,后面的和公平锁一样。

也就是公平锁与非公平锁的区别是: 当线程获取锁是直接参与竞争还是不参与直接插入队尾。

重入与不可重入

可重入锁的实现:当获取state不等于0,以及持有锁的线程等于当前线程则state加1,释放的时候state减1,直到为0。

可重入锁:同一个线程可以多次获取同一把锁

不可重入锁;同一个线程如果要再次获取锁,必须等待之前获取到的锁释放,不可重入锁很容易引入死锁。

自旋锁

一个线程尝试获取锁,获取不到,不会立即阻塞,而是采用循环的方式尝试获取,acquiedQueued方法中的for(;;)就是在自旋,自旋旋可以减少线程切换的上下文开销,但是如果自旋时间过长会非常耗费cpu的性能。

乐观与悲观锁

悲观锁

在操作资源前先加锁,例如数据库中的行锁,一个线程读取到准备操作资源,其他线程只能阻塞等待了。

悲观锁认为别人在操作资源的时候会对资源进行修改,所以在它持有资源的时候,会阻塞其他线程。

select col from table where col1 > 1 for update

乐观锁

乐观锁通过版本号来实现对资源的修改,当版本号不对则重新读取再次修改,不会阻塞其他线程。

如线程A,B同时读取到资源id = 2, data = 1, 他们都做加一操作:
则:
A: update set data = 2 , version = version +1 where id = 2 and version =1 成功
B: update set data =2, version =version +1 where id = 2 and verison =1 失败

CAS: compare and set.也是乐观锁,因为CAS没有版本号所以容易发生ABA问题。

参考

从ReenttrantLock的实现看AQS的原理及应用

java并发之AQS详解

java常见锁类型

以上是关于java并发包的基石:AbstractQueuedSychronier及synchornized的主要内容,如果未能解决你的问题,请参考以下文章

java并发包的基石:AbstractQueuedSychronier及synchornized

Java并发包基石-AQS详解

Java并发包基石-AQS详解

Java并发包基石-AQS详解

java基础Java并发包

JDK1.8并发包之 -- CountDownLatch