The devil is in the details, understand the underlying achieve AQS Java Concurrency

The JUC jdk package (java.util.concurrent) provide a large number of concurrent Java tools use basic written by Doug Lea, a lot to learn and learn, the only way is the advanced upgrade

Used herein JUC package from the object lock, the concurrent use of tools and features to start, with the problem, from shallow to deep, a step by step analysis of concurrent underlying abstract embodied AQS

Glossary

1 AQS

AQS is an abstract class, the full path java.util.concurrent.locks.AbstractQueuedSynchronizer class, abstract queue synchronizer, is based on the template model development tools concurrent abstract class, based on the following classes implement concurrent AQS:

2 CASES

CAS is an abbreviation Conmpare And Swap (compare and swap) is an atomic operation instruction

Which use CAS mechanism of three basic operand: memory address addr, the expected old value oldVal, to modify the new value newVal update a variable, the actual value only if the variable is the expected value oldVal and memory address addr which is the same , will modify the memory address addr corresponding value newVal

Based on optimistic locking the idea, by CAS continues to try and compare, you can update the values ​​of variables thread safe

3 thread interrupts

Interrupt thread is a thread coordination mechanism for the implementation of collaborative tasks other thread interrupts

When the thread is blocked waiting for the state, such as call wait (), join (), sleep () method, after calling interrupt thread () method, the thread exits blocked and will immediately receive an InterruptedException;

When the interrupt thread is running, the calling thread () method, thread and will not immediately break execution, require () method to detect thread interrupt flag by calling isInterrupted perform specific tasks in a logical thread, and then take the initiative to respond to interrupts, usually It is to throw InterruptedException

Object locking feature

The following first introduce the basic characteristics of what the object lock, complicated tools, back then gradually expand to realize how these features

Explicit get 1

ReentrantLock to lock, for example, supports the following four main ways to get explicit lock

  • (1) blocked waiting to acquire
ReentrantLock lock = new ReentrantLock();
// 一直阻塞等待,直到获取成功
lock.lock();
复制代码
  • (2) non-blocking attempt to acquire
ReentrantLock lock = new ReentrantLock();
// 尝试获取锁,如果锁已被其他线程占用,则不阻塞等待直接返回false
// 返回true - 锁是空闲的且被本线程获取,或者已经被本线程持有
// 返回false - 获取锁失败
boolean isGetLock = lock.tryLock();
复制代码
  • (3) blocked waiting to acquire a specified time
ReentrantLock lock = new ReentrantLock();
try {
    // 尝试在指定时间内获取锁
    // 返回true - 锁是空闲的且被本线程获取,或者已经被本线程持有
    // 返回false - 指定时间内未获取到锁
    lock.tryLock(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    // 内部调用isInterrupted() 方法检测线程中断标志位,主动响应中断
    e.printStackTrace();
}
复制代码
  • (4) in response to interrupt acquisition
ReentrantLock lock = new ReentrantLock();
try {
    // 响应中断获取锁
    // 如果调用线程的thread.interrupt()方法设置线程中断,线程退出阻塞等待并抛出中断异常
    lock.lockInterruptibly();
} catch (InterruptedException e) {
    e.printStackTrace();
}
复制代码

2 explicit release

ReentrantLock lock = new ReentrantLock();
lock.lock();
// ... 各种业务操作
// 显式释放锁
lock.unlock();
复制代码

3 reentrant

Has been acquired to lock the thread, again requesting the lock can be obtained directly

4 Shareable

Refer to the same resource allows multiple threads to share, such as a read-write lock locks allow multiple threads sharing, shared lock allows multiple threads concurrently secure access to data, improve the efficiency of program execution

5 fair, unfair

Fair locks: multiple threads first-come a fair way to compete lock. Before each lock wait queue will check there are no threads line up, will not try to acquire the lock. Unfair lock: When a thread non-fair way to acquire the lock, the thread will first try to obtain a lock instead of waiting. If you do not succeed, will enter the waiting queue

Because the non-lock fair ways to make subsequent thread has a chance to directly acquire the lock, reducing the chance of thread hangs waiting, is better than a fair lock

The principle AQS

1 Basic Concepts

(1) Condition Interface

Similar Object's wait (), wait (long timeout), notify () and notifyAll () method of binding synchronized built-in lock can be realized may be implemented wait / notification model, and ReentrantLock Lock interface, ReentrantReadWriteLock with other object lock has a similar function:

Condition interface defines await (), awaitNanos (long), signal (), signalAll () or the like, with the object lock example implementation wait / notification function principle is based AQS inner class ConditionObject implement the Condition interface, blocking the thread await and into CLH queue (mentioned below), wait for the other thread calls the method after the wake-up signal

(2) CLH queue

CLH queue, CLH is the algorithm author Craig, Landin, Hagersten name abbreviation

AQS maintains a two-way internal FIFO queue of CLH, AQS rely on it to manage the waiting thread, if the thread gets synchronized competition for resources fail, the thread will be blocked, and added to CLH synchronous queue; when the resource is free competition, based on CLH queue blocking thread and allocating resources

The head node to save the current CLH thread resource intensive, or there is no thread information, the other nodes save queuing thread information

CLH

CLH state of each node (waitStatus) values ​​are as follows:

  • CANCELED (1) : indicates that the current node has been canceled scheduled. After the node when (in the case interrupt response) timeout or interrupted, it will trigger changes to this state, entering the state will not change
  • The SIGNAL (-1) : represents a successor node in the current node waiting for wakeup. Before successor node into the dormant state into the team, it will be the precursor node status updates SIGNAL
  • CONDITION (-2) : represents a node waits on the Condition, Condition when another thread calls after the signal () method, from the node CONDITION status waiting queue of the synchronization queue, waiting to acquire synchronization lock
  • PROPAGATE (-3) : In shared mode, the node will not only wake of its predecessor successor node, but may also wake up the successor of the successor node
  • 0 : default when a new node into the team

(3) resource sharing

AQS defines two resource sharing: Exclusive exclusive, only one thread can perform, such as ReentrantLock Share Share, multiple threads can be executed simultaneously, such as Semaphore / CountDownLatch

(4) blocking / wake-threaded approach

AQS wake threads park method blocks thread sun.misc.Unsafe class provides, unpark based approach, method park blocked thread can respond to interrupt () interrupt request to exit blocked

2 Basic Design

The core design ideas: AQS provide a framework for implementing the queues dependent on the blocking locks and related CLH concurrent synchronizer. Subclass by implementing determine whether the acquisition / release methods protect resources, AQS achieve these protect queued thread based, awakened thread scheduling policy

AQS also provides a support thread-safe atomic update of type int variable as the value of the synchronization status (state), sub-categories according to actual needs, meaning that the variable represents a flexible definition updates

Methods series protect subclass redefined as follows:

  • boolean tryAcquire (int) attempts to acquire resources exclusively Returns true if successful, false if it fails
  • boolean tryRelease (int) attempt to free resources exclusively Returns true if successful, false if it fails
  • int tryAcquireShared (int) attempts to acquire resource sharing. A negative number indicates a failure; 0 indicates success, but there is no remaining available resources; positive number indicates success, and there is surplus resources
  • boolean tryReleaseShared (int) attempt to free sharing of resources, if allowed to release the follow-up wait wake node returns true, false otherwise

These methods always need to call to be scheduled by the cooperating thread, subclass shall be a non-blocking manner redefine these methods

AQS tryXXX the above method, based on the following methods to provide external acquire / release resources:

  • void acquire (int) exclusively to obtain resources, the thread is returned directly, or enter the queue, until resources get so far, and the whole process ignore the impact of disruption
  • boolean release next (int) exclusively thread to release resources to release a specified amount of resources, if completely released (ie state = 0), it will wake up in the queue waiting for other threads to access resources
  • void acquireShared (int) exclusive access to resources
  • boolean releaseShared (int) sharing resources released

In exclusive mode, for example: acquire / release to achieve the core resources as follows:

 Acquire:
     while (!tryAcquire(arg)) {
        如果线程尚未排队,则将其加入队列;
     }

 Release:
     if (tryRelease(arg))
        唤醒CLH中第一个排队线程
复制代码

Here, a little bit around, below a picture of the design ideas presented above to re-stroked a stroke:

AQS basic design

Characteristics achieve

Here are a series of principles to achieve functional properties of object-based lock AQS, concurrent tool

Explicit get 1

This feature is an example to lock ReentrantLock, ReentrantLock is reentrant object lock, each thread requesting a lock acquisition success, state synchronization state value plus 1, minus 1 releases the lock state, state 0 indicates no any thread holds the lock

ReentrantLock lock support fair / unfair characteristics, the following features to get explicit lock, for example fair

(1) blocked waiting to acquire

Basically as follows:

  • 1, ReentrantLock achieve the tryAcquire AQS (int) method, to determine: If no thread holds the lock, or the current thread already holds the lock, it returns true, false otherwise
  • 2, AQS to acquire (int) method for determining whether the current node is the head and based tryAcquire (int) availability of resources, if not available, the blocked waiting for queuing added CLH
  • 3, ReentrantLock the lock () method is based acquire AQS (int) method blocks waiting to acquire a lock

ReentrantLock in tryAcquire (int) method implementation:

protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
    // 没有任何线程持有锁
	if (c == 0) {
        // 通过CLH队列的head判断没有别的线程在比当前更早acquires
        // 且基于CAS设置state成功(期望的state旧值为0)
		if (!hasQueuedPredecessors() &&
			compareAndSetState(0, acquires)) {
            // 设置持有锁的线程为当前线程
			setExclusiveOwnerThread(current);
			return true;
		}
	}
    // 持有锁的线程为当前线程
	else if (current == getExclusiveOwnerThread()) {
        // 仅仅在当前线程,单线程,不用基于CAS更新
		int nextc = c + acquires;
		if (nextc < 0)
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
    // 其他线程已经持有锁
	return false;
}
复制代码

AQS's acquire (int) method implementation

public final void acquire(int arg) {
        // tryAcquire检查释放能获取成功
        // addWaiter 构建CLH的节点对象并入队
        // acquireQueued线程阻塞等待
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // acquireQueued返回true,代表线程在获取资源的过程中被中断
        // 则调用该方法将线程中断标志位设置为true
		selfInterrupt();
}


final boolean acquireQueued(final Node node, int arg) {
    // 标记是否成功拿到资源
	boolean failed = true;
	try {
	    // 标记等待过程中是否被中断过
		boolean interrupted = false;
		// 循环直到资源释放
		for (;;) {
		    // 拿到前驱节点
			final Node p = node.predecessor();
			
			// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源
			// 可能是head释放完资源唤醒本节点,也可能被interrupt()
			if (p == head && tryAcquire(arg)) {
			    // 成功获取资源
				setHead(node);
				// help GC
				p.next = null; 
				failed = false;
				return interrupted;
			}
			
			// 需要排队阻塞等待
			// 如果在过程中线程中断,不响应中断
			// 且继续排队获取资源,设置interrupted变量为true
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}
复制代码

(2) non-blocking attempt to acquire

Achieve tryLock ReentrantLock in () is only a non-fair locks for, implementing logical consistent with to tryAcquire, except that if there is no () Check CLH queue by hasQueuedPredecessors head of other threads are waiting, so when the resource is released when there is a thread request resources can jump the queue priority access

In ReentrantLock tryLock () embodied as follows:

public boolean tryLock() {
	return sync.nonfairTryAcquire(1);
}

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	// 没有任何线程持有锁
	if (c == 0) {
	    // 基于CAS设置state成功(期望的state旧值为0)
		// 没有检查CLH队列中是否有线程在等待
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 持有锁的线程为当前线程
	else if (current == getExclusiveOwnerThread()) {
	    // 仅仅在当前线程,单线程,不用基于CAS更新
		int nextc = c + acquires;
		if (nextc < 0) // overflow,整数溢出
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	// 其他线程已经持有锁
	return false;
}
复制代码

(3) blocked waiting to acquire a specified time

Basically as follows:

  • 1, ReentrantLock of tryLock (long, TimeUnit) AQS call the tryAcquireNanos (int, long) method
  • 2, AQS's tryAcquireNanos first call tryAcquire (int) try to get, get less then call doAcquireNanos (int, long) method
  • 3, AQS doAcquireNanos the current node determines whether the head and based tryAcquire (int) availability of resources, and can not be obtained if the timeout period is greater than 1 microsecond, then the attempt to obtain a period of time after sleep

Realization ReentrantLock follows:

public boolean tryLock(long timeout, TimeUnit unit)
		throws InterruptedException {
	return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
		throws InterruptedException {
	// 如果线程已经被interrupt()方法设置中断	
	if (Thread.interrupted())
		throw new InterruptedException();
	// 先tryAcquire尝试获取锁	
	return tryAcquire(arg) ||
		doAcquireNanos(arg, nanosTimeout);
}
复制代码

AQS are implemented as follows:

private boolean doAcquireNanos(int arg, long nanosTimeout)
		throws InterruptedException {
	if (nanosTimeout <= 0L)
		return false;
	// 获取到资源的截止时间	
	final long deadline = System.nanoTime() + nanosTimeout;
	final Node node = addWaiter(Node.EXCLUSIVE);
	// 标记是否成功拿到资源
	boolean failed = true;
	try {
		for (;;) {
		    // 拿到前驱节点
			final Node p = node.predecessor();
			// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源
            // 可能是head释放完资源唤醒本节点,也可能被interrupt()
			if (p == head && tryAcquire(arg)) {
			    // 成功获取资源
				setHead(node);
				// help GC
				p.next = null; 
				failed = false;
				return true;
			}
			// 更新剩余超时时间
			nanosTimeout = deadline - System.nanoTime();
			if (nanosTimeout <= 0L)
				return false;
			// 排队是否需要排队阻塞等待	
			// 且超时时间大于1微秒,则线程休眠到超时时间到了再尝试获取
			if (shouldParkAfterFailedAcquire(p, node) &&
				nanosTimeout > spinForTimeoutThreshold)
				LockSupport.parkNanos(this, nanosTimeout);

			// 如果线程已经被interrupt()方法设置中断
			// 则不再排队,直接退出 	
			if (Thread.interrupted())
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}
复制代码

(4) in response to interrupt acquisition

ReentrantLock interrupt response acquiring the lock is: When a thread response thead.interrupt () method in the park Sleep interrupt wake-up method, check the thread interrupt flag is true, an exception is thrown initiative, implemented in the core of AQS doAcquireInterruptibly (int) methods

And basically blocked waiting to acquire similar, just call from the AQS acquire (int) method instead of calling AQS doAcquireInterruptibly (int) method

private void doAcquireInterruptibly(int arg)
	throws InterruptedException {
	final Node node = addWaiter(Node.EXCLUSIVE);
	// 标记是否成功拿到资源
	boolean failed = true;
	try {
		for (;;) {
		    // 拿到前驱节点
			final Node p = node.predecessor();
			
			// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源
			// 可能是head释放完资源唤醒本节点,也可能被interrupt()
			if (p == head && tryAcquire(arg)) {
			    // 成功获取资源
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return;
			}
			
			// 需要排队阻塞等待
			if (shouldParkAfterFailedAcquire(p, node) &&
			    // 从排队阻塞中唤醒,如果检查到中断标志位为true
				parkAndCheckInterrupt())
				// 主动响应中断
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}
复制代码

2 explicit release

AQS resource sharing is divided into exclusive and shared, where the first to ReentrantLock example to introduce an explicit release exclusive resources, shared will be introduced later to

Obtaining explicit were similar, ReentrantLock explicitly freed basically as follows:

  • 1, ReentrantLock achieve AQS of tryRelease (int) method, the method will state variable minus 1, if the state becomes 0 means no thread holds the lock, returns true, false otherwise
  • 2, AQS the release (int) method is based on tryRelease (int) queue if there is any thread holds a resource, and if not, the thread CLH queue head node wakes up
  • 3, after the thread was awakened continue acquireQueued (Node, int) or doAcquireNanos (int, long) or in doAcquireInterruptibly (int) in for (;;) logic, continue to try to acquire resources

In ReentrantLock tryRelease (int) method implementation is as follows:

protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
	// 只有持有锁的线程才有资格释放锁
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
		
	// 标识是否没有任何线程持有锁	
	boolean free = false;
	
	// 没有任何线程持有锁
	// 可重入锁每lock一次都需要对应一次unlock
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}
复制代码

AQS the release (int) method implementation is as follows:

public final boolean release(int arg) {
    // 尝试释放资源
	if (tryRelease(arg)) {
		Node h = head;
		// 头节点不为空
		// 后继节点入队后进入休眠状态之前,会将前驱节点的状态更新为SIGNAL(-1)
		// 头节点状态为0,代表没有后继的等待节点
		if (h != null && h.waitStatus != 0)
		    // 唤醒第二个节点
		    // 头节点是占用资源的线程,第二个节点才是首个等待资源的线程
			unparkSuccessor(h);
		return true;
	}
	return false;
}
复制代码

3 reentrant

Reentrant implementation is relatively simple to ReentrantLock for example, is to achieve in tryAcquire (int) method, the thread holding the lock is not the current thread, and if so, update synchronization status value state, and returns true, the representative can get lock

4 Shareable

Can share resources to ReentrantReadWriteLock for example, with an exclusive lock ReentrantLock difference lies mainly, when it is retrieved, shared read locks allow multiple threads, when a write lock is released, more congestion waiting to be read simultaneously lock the thread can get to

state synchronization status values ​​are defined in the class ReentrantReadWriteLock of AQS, a high number of 16-bit read lock is held, the lower 16 bits of the write lock holding the lock

ReentrantReadWriteLock in tryAcquireShared (int), tryReleaseShared (int) logic longer implemented, mainly related to reading and writing are mutually exclusive, reentrant judgment, read lock to a write lock concessions, limited space, here is not started

Acquiring a read lock (ReadLock.lock ()) is mainly to achieve the following :

  • 1, ReentrantReadWriteLock achieve the AQS tryAcquireShared (int) method, it is determined whether the current thread to obtain read locks
  • 2, AQS's acquireShared (int) to access to resources based on tryAcquireShared (int) attempt, if the acquisition fails, join CLH queuing block waiting
  • 3, ReentrantReadWriteLock the ReadLock.lock () based on the AQS acquireShared (int) method blocks waiting to acquire a lock

AQS shared access to resources specific pattern to achieve the following:

public final void acquireShared(int arg) {
    // tryAcquireShared返回负数代表获取共享资源失败
	// 则通过进入等待队列,直到获取到资源为止才返回
	if (tryAcquireShared(arg) < 0)
		doAcquireShared(arg);
}

// 与前面介绍到的acquireQueued逻辑基本一致
// 不同的是将tryAcquire改为tryAcquireShared
// 还有资源获取成功后将传播给CLH队列上等待该资源的节点
private void doAcquireShared(int arg) {
	final Node node = addWaiter(Node.SHARED);
	 // 标记是否成功拿到资源
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head) {
				int r = tryAcquireShared(arg);
				// 资源获取成功
				if (r >= 0) {
 				    // 传播给CLH队列上等待该资源的节点                             
					setHeadAndPropagate(node, r);
					p.next = null; // help GC
					if (interrupted)
						selfInterrupt();
					failed = false;
					return;
				}
			}
			// 需要排队阻塞等待
            // 如果在过程中线程中断,不响应中断
            // 且继续排队获取资源,设置interrupted变量为true
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

//  资源传播给CLH队列上等待该资源的节点 
private void setHeadAndPropagate(Node node, int propagate) {
	Node h = head; 
	setHead(node);
	if (propagate > 0 || h == null || h.waitStatus < 0 ||
		(h = head) == null || h.waitStatus < 0) {
		Node s = node.next;
		if (s == null || s.isShared())
		    // 释放共享资源
			doReleaseShared();
	}
}
复制代码

Release read lock (ReadLock.unlock ()) is mainly to achieve the following : release ReentrantReadWriteLock shared resources primarily to achieve the following:

  • 1, ReentrantReadWriteLock achieve AQS of tryReleaseShared (int) method, is there a thread holds a read lock is released after the judge read lock
  • 2, AQS of releaseShared (int) based tryReleaseShared (int) determines whether a sleep queue thread CLH, if desired implementation doReleaseShared ()
  • 3, ReentrantReadWriteLock the ReadLock.unlock () based on the releaseShared AQS (int) method releases the lock

AQS shared mode release specific resources to achieve the following:

public final boolean releaseShared(int arg) {
    // 允许唤醒CLH中的休眠线程
	if (tryReleaseShared(arg)) {
	    // 执行资源释放
		doReleaseShared();
		return true;
	}
	return false;
}
	
private void doReleaseShared() {
	for (;;) {
		Node h = head;
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			// 当前节点正在等待资源
			if (ws == Node.SIGNAL) {
			    // 当前节点被其他线程唤醒了
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;            
				unparkSuccessor(h);
			}
			// 进入else的条件是,当前节点刚刚成为头节点
			// 尾节点刚刚加入CLH队列,还没在休眠前将前驱节点状态改为SIGNAL
			// CAS失败是尾节点已经在休眠前将前驱节点状态改为SIGNAL
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;               
		}
		// 每次唤醒后驱节点后,线程进入doAcquireShared方法,然后更新head
		// 如果h变量在本轮循环中没有被改变,说明head == tail,队列中节点全部被唤醒
		if (h == head)                 
			break;
	}
}
复制代码

5 fair, unfair

This feature is relatively simple to ReentrantLock lock, for example, directly based on a fair lock of AQS acquire (int) access to resources, and not fair to try to jump the queue lock: based CAS, the desired synchronization state variable value of 0 (no thread holds a lock ), updated to 1, if the update fails during CAS queue

// 公平锁实现
final void lock() {
	acquire(1);
}

// 非公平锁实现
final void lock() {
    // state值为0代表没有任何线程持有锁,直接插队获得锁
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1);
}
复制代码

to sum up

Meaning the value of the state variable AQS not necessarily represent resources, different subclasses of AQS can have different definitions of state variable values

For example, in countDownLatch class, state variable represents the need to release the latch count (the number can be understood as the need to open the door), requires each door are open, the door to open, all waiting threads will be started every time countDown ( ) will be on the state variable minus 1, if the state variable is decremented to zero, the wake-sleep queue thread CLH

Similar study the underlying source code is recommended to set a few problems, with learning problems; former popular learning is recommended that you thoroughly understand the overall design, the overall principle (you can read the relevant documentation), and then study the details of the source code, avoiding the outset tucked source easily without success

Guess you like

Origin juejin.im/post/5d9c5d63f265da5b905ee8fb